I got as far as using the new BTLE framework created in 2022.08 to scan and find my toothbrush as a test - this means an integration is possible with a BT interface connected to HASS itself:
Oral-b toothbrushes are already somewhat supported by the BLE Passive Montior add-on.
I am searching for a way to add support on ESP32 because I don’t have bluetooth dongle on my home assistant installation.
It kinda worked, but I had problems. I only posted the python code in case it helps someone who is interested on porting it to ESP32.
In case you’ve not seen the 2022.09 release video, ESPHome Bluetooth proxies were announced along with a lot of support for local/ remote/ proxy Bluetooth receivers.
Looks like add one line to an ESPhome config on an ESP32 to create a proxy receiver, although wired Ethernet might be an advantage (apparently WLAN + BT share one aerial so may multiplex).
I’d suggest watching the 2022.09 release video for lots of detail and demos of ESP32 acting as remote Bluetooth receivers with the ideas behind the features and future development.
I’ve created an integration for OralB toothbrush in this PR:
RE this block - on my Smart 7 tooth brush I was getting random values which had no relation with the front power display (189%, 253%, 128% etc)
sensor:
- platform: ble_client
ble_client_id: oralb1
name: "OralB1 battery level"
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'A0F0FF05-5047-4D53-8208-4F72616C2D42'
icon: 'mdi:battery'
unit_of_measurement: '%'
update_interval: 4s
lambda: |-
return x[1];
According to this page OralBlue_python/Protocol.md at 15e1a03bcb3350574d438e4593bcff59608a77a7 · wise86-android/OralBlue_python · GitHub the second and third bytes are an LE representation of seconds remaining. Changing it to return x[0]
gets what look like sensible values.
On my brush the brushing time using the above is in minutes, looking at the same link it appears this is another two byte field - x[0] minutes, and x[1] seconds, and indeed if I do x[0] * 60 + x[1] I get sensible values.
Seeing as it looks like this is going to make it into Home Assistant nativly I thought I should flag before it does in case those havent yet been fixed.
My full config is
substitutions:
devicename: mortara
upper_devicename: "Mortara"
toothbrush_name: "Kev's Toothbrush"
esphome:
name: $devicename
platform: ESP32
board: nodemcu-32s
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_passwd
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "{$upper_devicename} Fallback Hotspot"
password: !secret fallback_passwd
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
switch:
- platform: restart
name: "$upper_devicename Restart"
ble_client:
- mac_address: "XX:XX:XX:XX:XX:XX"
id: oralb_kev
binary_sensor:
- platform: ble_presence
mac_address: "XX:XX:XX:XX:XX:XX"
name: "$toothbrush_name Online"
sensor:
- platform: wifi_signal
name: "$upper_devicename - WiFi Signal Sensor"
update_interval: 60s
- platform: uptime
name: "$upper_devicename - Uptime Sensor"
- platform: ble_client
ble_client_id: oralb_kev
name: "$toothbrush_name battery level"
type: characteristic
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'A0F0FF05-5047-4D53-8208-4F72616C2D42'
icon: 'mdi:battery'
unit_of_measurement: '%'
update_interval: 4s
lambda: |-
return x[0];
- platform: ble_client
ble_client_id: oralb_kev
type: characteristic
name: "$toothbrush_name brush time"
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'a0f0ff08-5047-4d53-8208-4f72616c2d42'
icon: 'mdi:timer'
unit_of_measurement: 's'
update_interval: 1s
lambda: |-
return (x[0] * 60) + x[1];
- platform: ble_client
ble_client_id: oralb_kev
type: characteristic
name: "$toothbrush_name status ID"
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'a0f0ff04-5047-4d53-8208-4f72616c2d42'
icon: 'mdi:timer'
update_interval: 4s
lambda: |-
switch( x[0] )
{
case 2:
id(kev_status).publish_state(std::string("IDLE"));
break;
case 3:
id(kev_status).publish_state(std::string("RUN"));
break;
default:
id(kev_status).publish_state(std::string("UNKNOWN"));
}
return x[0];
- platform: ble_client
ble_client_id: oralb_kev
type: characteristic
name: "$toothbrush_name mode ID"
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'a0f0ff07-5047-4d53-8208-4f72616c2d42'
icon: 'mdi:timer'
update_interval: 4s
lambda: |-
switch( x[0] )
{
case 0:
id(kev_mode).publish_state(std::string("OFF"));
break;
case 1:
id(kev_mode).publish_state(std::string("DAILY_CLEAN"));
break;
case 2:
id(kev_mode).publish_state(std::string("SENSITIVE"));
break;
case 3:
id(kev_mode).publish_state(std::string("GUM_CARE"));
break;
case 4:
id(kev_mode).publish_state(std::string("WHITEN"));
break;
case 5:
id(kev_mode).publish_state(std::string("DEEP_CLEAN"));
break;
case 6:
id(kev_mode).publish_state(std::string("TOUNGE_CLEAN"));
break;
case 7:
id(kev_mode).publish_state(std::string("INTENSE"));
break;
default:
id(kev_mode).publish_state(std::string("UNKNOWN"));
}
return x[0];
text_sensor:
- platform: version
name: "$upper_devicename - ESPHome Version"
- platform: template
name: "$toothbrush_name status"
id: kev_status
- platform: template
name: "$toothbrush_name mode"
id: kev_mode
Maybe it is not base-10? or maybe it is on a scale from 0 to 255? Something similar?
Just an idea.
Next line said what it probably was - “the second and third bytes are an LE representation of seconds remaining.”. Decoding two byte stuff on ESPHome is beyond me!
It’s late and I haven’t seen the data but If it’s a little-endian, couldn’t it be decoded with something like this x[0] + x[1]*255 ?
Ta, had never messed with Little Endian so no idea how it was parsed and a quick search wasn’t being helpful.
Changing the battery sensor to the following
- platform: ble_client
ble_client_id: oralb_kev
name: "$toothbrush_name battery level"
type: characteristic
service_uuid: 'a0f0ff00-5047-4d53-8208-4f72616c2d42'
characteristic_uuid: 'A0F0FF05-5047-4D53-8208-4F72616C2D42'
icon: 'mdi:battery'
unit_of_measurement: '%'
update_interval: 4s
lambda: |-
id(kev_seconds).publish_state(x[1] + (x[2] * 255));
return x[0];
(note the penultimate line) and adding the following sensor
- platform: template
name: "$toothbrush_name battery seconds"
id: kev_seconds
have now given me another entity which seems to work - a 120 second brush reduced the battery level by 71 seconds if this is “accurate”. Note it doesn’t update during the brush (unlike the %) only once you stop.
Awesome! I noticed the integration listed two brushes as supported, I just received an iO 10 yesterday. When I get a chance I’ll try it out and report back.
Yeah, I’ll be giving the integration a try too when it gets to 2022.11.4!
Got sick of using my my old Oral B brush and finding 10 seconds into a brush the battery would be flat! When it died found this thread and the mention of battery usage, so Bluetooth brush it is.
I just installed your Oral-B integration and hooked up my iO Series 10 and it seams to work.
But I noticed the following things:
- It’s identified as IO Series 7/8
- Tongue Cleaning shows up as “unknown mode 6”
- Pressure & Sector sensors have values even though the brush is inactive, maybe default to unknown while not brushing?
How can I help you to gather the data needed to correctly identify the series 10 brush?
It seems the toothbrush only transmitting the values when you turn it off, so these values are just the ones from the last session.
There is no transmission during the brush session.
Interestingly with the “proper” integration my 7000 shows up as a 6000 Series and doesn’t report battery level, but it has picked up a neighbour’s toothbrush which it identifies as a 7000!
Will stick with ESP Home for now - much more useful set of information
vs Home Assistant directly
@kevjs1982 love the information you get from ESPHome, could you share your configuration for these sensors you have on your 1st screenshot?
- Battery Left (secs)
- Brushes Today
- Last Charged
- Needs Charging
- etc…
It’s shared further up this thread
and the battery seconds was added here:-
Need’s Changing is a separate Input Helper which records the date I last changed the brush head and a template sensor in Home Assistant (this is from sensors.yaml - not the new style)
kevs_toothbrush_head_needs_changing: value_template: >- {%- if (as_timestamp(now())) > (((as_timestamp(states('input_datetime.toothbrush_head_changed')) + (90*24*3600)))) -%}yes{%- else %}no{%- endif %}
When that changes to “yes” an automation sends a notification to my phone this is in automation.yaml
- id: '1638906269550'
alias: Toothbrush Head Change Reminder
description: ''
use_blueprint:
path: task_due_enhanced.yaml
input:
due_task_entity: sensor.kevs_toothbrush_head_needs_changing
alert_notification_device: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
image_url: https://home.example.com/local/android/smile_teeth.jpg
entity_to_update: input_datetime.toothbrush_head_changed
and the blueprint for task_due_enhanced.yaml is
blueprint:
name: Task Reminder
description: Alert when a task is due (status is yes)
domain: automation
input:
due_task_entity:
name: Due Task (status is yes when due)
selector:
entity:
domain: sensor
alert_notification_device:
name: Which device to alert
selector:
device:
integration: mobile_app
image_url:
name: image_to_include_in_notification
entity_to_update:
name: Which entity to update (Date Input Helper with today's date)
selector:
entity:
domain: input_datetime
mode: single
max_exceeded: silent
variables:
due_task_entity: !input due_task_entity
entity_to_update: !input entity_to_update
action_done: "task_reminder_{{ remind_me_entity }}"
trigger:
- platform: state
entity_id: !input due_task_entity
condition:
- condition: state
entity_id: !input due_task_entity
state: "yes"
action:
- domain: mobile_app
type: notify
device_id: !input alert_notification_device
title: >
{{ state_attr(due_task_entity,'friendly_name')|replace('Does','')|trim }}
message: >
{{ state_attr(due_task_entity,'friendly_name')|replace('Does','')|replace('?','')|trim }} is due
data:
image: !input image_url
channel: !input due_task_entity
tag: !input due_task_entity
persistent: true
actions:
- action: "{{ action_done }}"
title: "Done"
# Wait for the user to select an action
- wait_for_trigger:
platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ action_done }}"
- service: input_datetime.set_datetime
target:
entity_id: !input entity_to_update
data:
date: '{{ now().strftime(''%Y-%m-%d'') }}'
# https://community.home-assistant.io/t/actionable-notifications-for-android/256773 See this for examples...
Thanks a lot for sharing this, I’m not familiar with lambda
at the moment and wondering if you had seen this error at all;
/config/esphome/esp32-bluetooth-proxy-f8f828.yaml:38:65: error: no matching function for call to 'esphome::template_::TemplateTextSensor::publish_state(int)'
id(name_toothbrush_seconds).publish_state(x[1] + (x[2] * 255));
^
In file included from src/esphome/core/controller.h:20,
from src/esphome/components/api/api_server.h:4,
from src/esphome/components/api/api_connection.h:6,
from src/esphome.h:3,
from src/main.cpp:3:
src/esphome/components/text_sensor/text_sensor.h:34:8: note: candidate: 'void esphome::text_sensor::TextSensor::publish_state(const string&)'
void publish_state(const std::string &state);
^~~~~~~~~~~~~
src/esphome/components/text_sensor/text_sensor.h:34:8: note: no known conversion for argument 1 from 'int' to 'const string&' {aka 'const std::__cxx11::basic_string<char>&'}
I’ve had a hunt around (and maybe I’m just being useless at search engine hunting) but haven’t come up with any suggestions on it
Well, from the error I can conclude that “name_toothbrush_seconds” is defined as a text_sensor, so the “publish_state” expects a string as parameter and not an integer. Without the rest of your yaml is not easy to say what the best way is to solve your problem.
Rest of the YAML is what’s above on the page in the post prior to mine, is why I didn’t include it, but thanks for the pointer.