bkbilly
(Vasilis Koulis)
September 8, 2022, 11:50am
1
I did a little bit of research to find out how to add my OralB bluetooth toothbrush to Home Assistant and I came up with this on ESPHome.
It doesn’t work perfectly and it makes my ESP32 restart by itself, but I am posting this in case someone figures out the bugs.
esphome:
name: smartlock
platform: ESP32
board: nodemcu-32s
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
logger:
ota:
api:
esp32_ble_tracker:
ble_client:
- mac_address: FF:FF:FF:FF:FF:FF
id: oralb1
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];
- platform: ble_client
ble_client_id: oralb1
name: "OralB1 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];
On my computer it works perfectly, so I am also attaching the pythong code that I am using:
import asyncio
from bleak import BleakClient
import time
chars = {
"a0f0ff08-5047-4d53-8208-4f72616c2d42": "time",
"a0f0ff05-5047-4d53-8208-4f72616c2d42": "battery",
"a0f0ff04-5047-4d53-8208-4f72616c2d42": "status",
"a0f0ff07-5047-4d53-8208-4f72616c2d42": "mode",
}
statuses = {
2: "IDLE",
3: "RUN",
}
modes = {
0: "OFF",
1: "DAILY_CLEAN",
7: "INTENSE",
2: "SENSITIVE",
4: "WHITEN",
3: "GUM_CARE",
6: "TONGUE_CLEAN",
}
async def main(address):
async with BleakClient(address) as client:
while True:
try:
print(dir(client))
cl_services = await client.get_services()
import ipdb
ipdb.set_trace()
tasks = []
for char, res_type in chars.items():
tasks.append(asyncio.create_task(client.read_gatt_char(char)))
results = await asyncio.gather(*tasks)
res_dict = dict(zip(chars.values(), results))
print(f"Brush Time: {60 * res_dict['time'][0] + res_dict['time'][1]}")
print(f"Battery: {res_dict['battery'][0]}")
print(f"Status: {statuses.get(res_dict['status'][0], 'UNKNOWN')}")
print(f"Mode: {modes.get(res_dict['mode'][0], 'UNKNOWN')}")
time.sleep(1)
print("\n")
except Exception as e:
print(e)
return 0
address = "FF:FF:FF:FF:FF:FF"
asyncio.run(main(address))
It would be great if we could make an integration for this.
1 Like
Hellis81
(Hellis81)
September 8, 2022, 12:18pm
2
I’m quite sure I have seen a thread about this before. I believe there is working code for it here on the forum.
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:
TL;DR - Supported BTLE devices with new-stye integrations are in GitHub with an explanation and plans in HACS
Basically, bdraco (J. Nick Koston) has created a set of BTLE foundations making it easier to create integrations for individual BTLE devices/ brands/ types (e.g. Bleak client + DBUS integration + BLE parser , etc).
Originally, he had a Passive BLE Monitor integration which lists lots of devices and can be installed from HACS. The current work is to create individual integrations for d…
WallyR
(Wally)
September 9, 2022, 12:26pm
4
Oral-b toothbrushes are already somewhat supported by the BLE Passive Montior add-on.
bkbilly
(Vasilis Koulis)
September 9, 2022, 1:11pm
5
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.
bkbilly
(Vasilis Koulis)
October 13, 2022, 7:11pm
7
I’ve created an integration for OralB toothbrush in this PR:
home-assistant:dev
← bkbilly:oralb
opened 07:07PM - 13 Oct 22 UTC
<!--
You are amazing! Thanks for contributing to our project!
Please, DO N… OT DELETE ANY TEXT from this template! (unless instructed).
-->
## Proposed change
<!--
Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug
or resolves a feature request, be sure to link to that issue in the
additional information section.
-->
Support for Bluetooth toothbrush by OralB which exposes the following sensors:
- Brush Time in seconds
- Battery Percentage
- Selected Mode
- Operation status
## Type of change
<!--
What type of change does your PR introduce to Home Assistant?
NOTE: Please, check only 1! box!
If your PR requires multiple boxes to be checked, you'll most likely need to
split it into multiple PRs. This makes things easier and faster to code review.
-->
- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New integration (thank you!)
- [ ] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
## Additional information
<!--
Details are important, and help maintainers processing your PR.
Please be sure to fill out additional details, if applicable.
-->
- This PR fixes or closes issue: fixes #
- This PR is related to issue:
- Link to documentation pull request:
## Checklist
<!--
Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to ask.
We're here to help! This is simply a reminder of what we are going to look
for before merging your code.
-->
- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] The code has been formatted using Black (`black --fast homeassistant tests`)
- [ ] Tests have been added to verify that the new code works.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
If the code communicates with devices, web services, or third-party tools:
- [x] The [manifest file][manifest-docs] has all fields filled out correctly.
Updated and included derived files by running: `python3 -m script.hassfest`.
- [x] New or updated dependencies have been added to `requirements_all.txt`.
Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.
The integration reached or maintains the following [Integration Quality Scale][quality-scale]:
<!--
The Integration Quality Scale scores an integration on the code quality
and user experience. Each level of the quality scale consists of a list
of requirements. We highly recommend getting your integration scored!
-->
- [ ] No score or internal
- [x] 🥈 Silver
- [ ] 🥇 Gold
- [ ] 🏆 Platinum
<!--
This project is very active and we have a high turnover of pull requests.
Unfortunately, the number of incoming pull requests is higher than what our
reviewers can review and merge so there is a long backlog of pull requests
waiting for review. You can help here!
By reviewing another pull request, you will help raise the code quality of
that pull request and the final review will be faster. This way the general
pace of pull request reviews will go up and your wait time will go down.
When picking a pull request to review, try to choose one that hasn't yet
been reviewed.
Thanks for helping out!
-->
To help with the load of incoming pull requests:
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure
<!--
Thank you for contributing <3
Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html
[docs-repository]: https://github.com/home-assistant/home-assistant.io
2 Likes
kevjs1982
(Kevjs1982)
October 30, 2022, 4:04pm
8
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
2 Likes
k8gg
October 31, 2022, 9:25am
9
Maybe it is not base-10? or maybe it is on a scale from 0 to 255? Something similar?
Just an idea.
kevjs1982
(Kevjs1982)
October 31, 2022, 10:02am
10
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!
ZyberSE
November 1, 2022, 9:45pm
11
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 ?
1 Like
kevjs1982
(Kevjs1982)
November 2, 2022, 8:08am
12
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.
ZyberSE
November 2, 2022, 8:31am
13
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.
kevjs1982
(Kevjs1982)
November 2, 2022, 8:47am
14
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.
ZyberSE
November 3, 2022, 8:30am
15
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?
WallyR
(Wally)
November 3, 2022, 9:23am
16
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.
kevjs1982
(Kevjs1982)
November 15, 2022, 10:36am
17
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
bkbilly
(Vasilis Koulis)
November 15, 2022, 11:04am
18
@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…
kevjs1982
(Kevjs1982)
November 15, 2022, 11:50am
19
It’s shared further up this thread
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 OralBlu…
and the battery seconds was added here:-
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…
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...
2 Likes
darkorb
(Alex)
January 5, 2023, 10:51pm
20
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