From 90c3cb62b37e9ce308ed0c9d510f54ff95c2dfec Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 3 May 2020 15:23:21 +0100 Subject: [PATCH] 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. --- .../components/esp32_ble_tracker/__init__.py | 2 +- esphome/components/oralb_ble/__init__.py | 18 +++++++ esphome/components/oralb_ble/oralb_ble.cpp | 48 +++++++++++++++++++ esphome/components/oralb_ble/oralb_ble.h | 27 +++++++++++ esphome/components/oralb_brush/__init__.py | 0 .../components/oralb_brush/oralb_brush.cpp | 19 ++++++++ esphome/components/oralb_brush/oralb_brush.h | 43 +++++++++++++++++ esphome/components/oralb_brush/sensor.py | 36 ++++++++++++++ 8 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 esphome/components/oralb_ble/__init__.py create mode 100644 esphome/components/oralb_ble/oralb_ble.cpp create mode 100644 esphome/components/oralb_ble/oralb_ble.h create mode 100644 esphome/components/oralb_brush/__init__.py create mode 100644 esphome/components/oralb_brush/oralb_brush.cpp create mode 100644 esphome/components/oralb_brush/oralb_brush.h create mode 100644 esphome/components/oralb_brush/sensor.py diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index c4cc7260fd..e3aaf29eea 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -9,7 +9,7 @@ from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ 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' diff --git a/esphome/components/oralb_ble/__init__.py b/esphome/components/oralb_ble/__init__.py new file mode 100644 index 0000000000..a54739dae7 --- /dev/null +++ b/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) diff --git a/esphome/components/oralb_ble/oralb_ble.cpp b/esphome/components/oralb_ble/oralb_ble.cpp new file mode 100644 index 0000000000..6b353e1778 --- /dev/null +++ b/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 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 diff --git a/esphome/components/oralb_ble/oralb_ble.h b/esphome/components/oralb_ble/oralb_ble.h new file mode 100644 index 0000000000..648d3edde7 --- /dev/null +++ b/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 state; +}; + +bool parse_oralb_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, OralbParseResult &result); + +optional 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 diff --git a/esphome/components/oralb_brush/__init__.py b/esphome/components/oralb_brush/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/oralb_brush/oralb_brush.cpp b/esphome/components/oralb_brush/oralb_brush.cpp new file mode 100644 index 0000000000..dc1262dc83 --- /dev/null +++ b/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 diff --git a/esphome/components/oralb_brush/oralb_brush.h b/esphome/components/oralb_brush/oralb_brush.h new file mode 100644 index 0000000000..652a9e476a --- /dev/null +++ b/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 diff --git a/esphome/components/oralb_brush/sensor.py b/esphome/components/oralb_brush/sensor.py new file mode 100644 index 0000000000..787fb44036 --- /dev/null +++ b/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))