Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add tracker for OralB toothbrushes
The OralB toothbrushes expose some of their information in their bluetooth advertisement data.

This data lets us see the state (idle, running), brush mode (daily clean, tongue, whitening, etc.), pressure and some other bits of data.

This component lets you expose that data with config as follows:

```
esp32_ble_tracker:

sensor:
  - platform: oralb_brush
    mac_address: 00:00:00:00:00:00
    state:
      name: "Toothbrush State"
```

Checkout https://github.com/zewelor/bt-mqtt-gateway/blob/master/workers/toothbrush_homeassistant.py and https://esphome.io/components/esp32_ble_tracker.html for more information.
  • Loading branch information
Ian committed May 3, 2020
1 parent 7fa5cab commit 90c3cb6
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 1 deletion.
2 changes: 1 addition & 1 deletion esphome/components/esp32_ble_tracker/__init__.py
Expand Up @@ -9,7 +9,7 @@
from esphome.core import coroutine

ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
AUTO_LOAD = ['xiaomi_ble', 'ruuvi_ble']
AUTO_LOAD = ['xiaomi_ble', 'ruuvi_ble', 'oralb_ble']

CONF_ESP32_BLE_ID = 'esp32_ble_id'
CONF_SCAN_PARAMETERS = 'scan_parameters'
Expand Down
18 changes: 18 additions & 0 deletions esphome/components/oralb_ble/__init__.py
@@ -0,0 +1,18 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import esp32_ble_tracker
from esphome.const import CONF_ID

DEPENDENCIES = ['esp32_ble_tracker']

oralb_ble_ns = cg.esphome_ns.namespace('oralb_ble')
OralbListener = oralb_ble_ns.class_('OralbListener', esp32_ble_tracker.ESPBTDeviceListener)

CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(OralbListener),
}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)


def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)
48 changes: 48 additions & 0 deletions esphome/components/oralb_ble/oralb_ble.cpp
@@ -0,0 +1,48 @@
#include "oralb_ble.h"
#include "esphome/core/log.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace oralb_ble {

static const char *TAG = "oralb_ble";

bool parse_oralb_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, OralbParseResult &result) {
result.state = adv_data[3];
return true;
}
optional<OralbParseResult> parse_oralb(const esp32_ble_tracker::ESPBTDevice &device) {
bool success = false;
OralbParseResult result{};
for (auto &it : device.get_manufacturer_datas()) {
bool is_oralb = it.uuid.contains(0xDC, 0x00);
if (!is_oralb)
continue;

if (parse_oralb_data_byte(it.data, result))
success = true;
}
if (!success)
return {};
return result;
}

bool OralbListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
auto res = parse_oralb(device);
if (!res.has_value())
return false;

ESP_LOGD(TAG, "Got OralB (%s):", device.address_str().c_str());

if (res->state.has_value()) {
ESP_LOGD(TAG, " State: %d", *res->state);
}

return true;
}

} // namespace oralb_ble
} // namespace esphome

#endif
27 changes: 27 additions & 0 deletions esphome/components/oralb_ble/oralb_ble.h
@@ -0,0 +1,27 @@
#pragma once

#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace oralb_ble {

struct OralbParseResult {
optional<uint8_t> state;
};

bool parse_oralb_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, OralbParseResult &result);

optional<OralbParseResult> parse_oralb(const esp32_ble_tracker::ESPBTDevice &device);

class OralbListener : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};

} // namespace oralb_ble
} // namespace esphome

#endif
Empty file.
19 changes: 19 additions & 0 deletions esphome/components/oralb_brush/oralb_brush.cpp
@@ -0,0 +1,19 @@
#include "oralb_brush.h"
#include "esphome/core/log.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace oralb_brush {

static const char *TAG = "oralb_brush";

void OralbBrush::dump_config() {
ESP_LOGCONFIG(TAG, "OralbBrush");
LOG_SENSOR(" ", "State", this->state_);
}

} // namespace oralb_brush
} // namespace esphome

#endif
43 changes: 43 additions & 0 deletions esphome/components/oralb_brush/oralb_brush.h
@@ -0,0 +1,43 @@
#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/oralb_ble/oralb_ble.h"

#ifdef ARDUINO_ARCH_ESP32

namespace esphome {
namespace oralb_brush {

class OralbBrush : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }

bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
if (device.address_uint64() != this->address_)
return false;

auto res = oralb_ble::parse_oralb(device);
if (!res.has_value())
return false;

if (res->state.has_value() && this->state_ != nullptr)
this->state_->publish_state(*res->state);

return true;
}

void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_state(sensor::Sensor *state) { state_ = state; }

protected:
uint64_t address_;
sensor::Sensor *state_{nullptr};
};

} // namespace oralb_brush
} // namespace esphome

#endif
36 changes: 36 additions & 0 deletions esphome/components/oralb_brush/sensor.py
@@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, esp32_ble_tracker
from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
CONF_PRESSURE, CONF_ACCELERATION, CONF_ACCELERATION_X, CONF_ACCELERATION_Y, \
CONF_ACCELERATION_Z, CONF_BATTERY_VOLTAGE, CONF_TX_POWER, \
CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, UNIT_CELSIUS, \
ICON_THERMOMETER, UNIT_PERCENT, UNIT_VOLT, UNIT_HECTOPASCAL, UNIT_G, \
UNIT_DECIBEL_MILLIWATT, UNIT_EMPTY, ICON_WATER_PERCENT, ICON_BATTERY, \
ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, ICON_ACCELERATION_Y, \
ICON_ACCELERATION_Z, ICON_SIGNAL, CONF_ID, ICON_EMPTY, CONF_STATE

DEPENDENCIES = ['esp32_ble_tracker']
AUTO_LOAD = ['oralb_ble']

oralb_brush_ns = cg.esphome_ns.namespace('oralb_brush')
OralbBrush = oralb_brush_ns.class_(
'OralbBrush', esp32_ble_tracker.ESPBTDeviceListener, cg.Component)

CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(OralbBrush),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_STATE): sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0),
}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)


def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield esp32_ble_tracker.register_ble_device(var, config)

cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))

if CONF_STATE in config:
sens = yield sensor.new_sensor(config[CONF_STATE])
cg.add(var.set_state(sens))

5 comments on commit 90c3cb6

@marji
Copy link

@marji marji commented on 90c3cb6 Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, this is awesome work, Could you please open a PR against the main esphome?
It would make it available to newcomers to the project. Many thanks for considering.

@KiLLeRRaT
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, this is awesome work, Could you please open a PR against the main esphome?
It would make it available to newcomers to the project. Many thanks for considering.

I'd love to also see this!

@ehn
Copy link

@ehn ehn commented on 90c3cb6 Jul 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this.

Failing that, what's the cleanest way to get this patch applied to the upstream version? Installing from this cloned repository instead doesn't seem like a good idea.

@MilanGajicBuva
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you still working on this?
I would love to see this as an HA integration.
I can imagine things like:

  • warning when brushing too hard
  • warning when battery low
  • a fireworks / clapping sound for the little one when 2minutes is up + enable internet again ;-)

@NODeeJay
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in esphome/components/esp32_ble_tracker/__init__.py are not needed anymore (see esphome#2617), the integration can be loaded in the esp yaml:

# Example config.yaml
esp32_ble_tracker:
oralb-ble:

The documentation says: "Just clone the repository locally, do the changes for your new feature/bug fix and submit a pull request."

In the ESP forum there is already a feature request (see esphome/feature-requests#1062) for the integration, however the procrastination level is comparable. I will continue in esphome/feature-requests#1062 with the next steps clone, change, test etc. I have a couple of ESPs and OralBs laying around and will try to get it working.
Any help from @ehn @MilanGajicBuva @KiLLeRRaT @marji is appreciated :-)

Please sign in to comment.