diff --git a/esphome/components/xiaomi_miscale/__init__.py b/esphome/components/xiaomi_miscale/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py new file mode 100644 index 0000000000..a5e91a9178 --- /dev/null +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_WEIGHT, UNIT_KILOGRAM, \ + ICON_SCALE_BATHROOM + +DEPENDENCIES = ['esp32_ble_tracker'] + +xiaomi_miscale_ns = cg.esphome_ns.namespace('xiaomi_miscale') +XiaomiMiscale = xiaomi_miscale_ns.class_('XiaomiMiscale', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiMiscale), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_WEIGHT): sensor.sensor_schema(UNIT_KILOGRAM, ICON_SCALE_BATHROOM, 2), +}).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_WEIGHT in config: + sens = yield sensor.new_sensor(config[CONF_WEIGHT]) + cg.add(var.set_weight(sens)) diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp new file mode 100644 index 0000000000..441bca6270 --- /dev/null +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -0,0 +1,101 @@ +#include "xiaomi_miscale.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miscale { + +static const char *TAG = "xiaomi_miscale"; + +void XiaomiMiscale::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Miscale"); + LOG_SENSOR(" ", "Weight", this->weight_); +} + +bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = parse_header(service_data); + if (!res.has_value()) { + continue; + } + if (!(parse_message(service_data.data, *res))) { + continue; + } + if (!(report_results(res, device.address_str()))) { + continue; + } + if (res->weight.has_value() && this->weight_ != nullptr) + this->weight_->publish_state(*res->weight); + success = true; + } + + return success; +} + +optional XiaomiMiscale::parse_header(const esp32_ble_tracker::ServiceData &service_data) { + ParseResult result; + if (!service_data.uuid.contains(0x1D, 0x18)) { + ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + return {}; + } + + return result; +} + +bool XiaomiMiscale::parse_message(const std::vector &message, ParseResult &result) { + // exemple 1d18 a2 6036 e307 07 11 0f1f11 + // 1-2 Weight (MISCALE 181D) + // 3-4 Years (MISCALE 181D) + // 5 month (MISCALE 181D) + // 6 day (MISCALE 181D) + // 7 hour (MISCALE 181D) + // 8 minute (MISCALE 181D) + // 9 second (MISCALE 181D) + + const uint8_t *data = message.data(); + const int data_length = 10; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + // weight, 2 bytes, 16-bit unsigned integer, 1 kg + const int16_t weight = uint16_t(data[1]) | (uint16_t(data[2]) << 8); + if (data[0] == 0x22 || data[0] == 0xa2) + result.weight = weight * 0.01f / 2.0f; // unit 'kg' + else if (data[0] == 0x12 || data[0] == 0xb2) + result.weight = weight * 0.01f * 0.6; // unit 'jin' + else if (data[0] == 0x03 || data[0] == 0xb3) + result.weight = weight * 0.01f * 0.453592; // unit 'lbs' + + return true; +} + +bool XiaomiMiscale::report_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got Xiaomi Miscale (%s):", address.c_str()); + + if (result->weight.has_value()) { + ESP_LOGD(TAG, " Weight: %.2fkg", *result->weight); + } + + return true; +} + +} // namespace xiaomi_miscale +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h new file mode 100644 index 0000000000..d9da4f9421 --- /dev/null +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miscale { + +struct ParseResult { + optional weight; +}; + +class XiaomiMiscale : 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; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_weight(sensor::Sensor *weight) { weight_ = weight; } + + protected: + uint64_t address_; + sensor::Sensor *weight_{nullptr}; + + optional parse_header(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message(const std::vector &message, ParseResult &result); + bool report_results(const optional &result, const std::string &address); +}; + +} // namespace xiaomi_miscale +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miscale2/__init__.py b/esphome/components/xiaomi_miscale2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py new file mode 100644 index 0000000000..9f32385d54 --- /dev/null +++ b/esphome/components/xiaomi_miscale2/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_WEIGHT, UNIT_KILOGRAM, \ + ICON_SCALE_BATHROOM, UNIT_OHM, CONF_IMPEDANCE, ICON_OMEGA + +DEPENDENCIES = ['esp32_ble_tracker'] + +xiaomi_miscale2_ns = cg.esphome_ns.namespace('xiaomi_miscale2') +XiaomiMiscale2 = xiaomi_miscale2_ns.class_('XiaomiMiscale2', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiMiscale2), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_WEIGHT): sensor.sensor_schema(UNIT_KILOGRAM, ICON_SCALE_BATHROOM, 2), + cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema(UNIT_OHM, ICON_OMEGA, 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_WEIGHT in config: + sens = yield sensor.new_sensor(config[CONF_WEIGHT]) + cg.add(var.set_weight(sens)) + if CONF_IMPEDANCE in config: + sens = yield sensor.new_sensor(config[CONF_IMPEDANCE]) + cg.add(var.set_impedance(sens)) diff --git a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp new file mode 100644 index 0000000000..2bc4656cb9 --- /dev/null +++ b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.cpp @@ -0,0 +1,116 @@ +#include "xiaomi_miscale2.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miscale2 { + +static const char *TAG = "xiaomi_miscale2"; + +void XiaomiMiscale2::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Miscale2"); + LOG_SENSOR(" ", "Weight", this->weight_); + LOG_SENSOR(" ", "Impedance", this->impedance_); +} + +bool XiaomiMiscale2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = parse_header(service_data); + if (!res.has_value()) { + continue; + } + if (!(parse_message(service_data.data, *res))) { + continue; + } + if (!(report_results(res, device.address_str()))) { + continue; + } + if (res->weight.has_value() && this->weight_ != nullptr) + this->weight_->publish_state(*res->weight); + if (res->impedance.has_value() && this->impedance_ != nullptr) + this->impedance_->publish_state(*res->impedance); + success = true; + } + + return success; +} + +optional XiaomiMiscale2::parse_header(const esp32_ble_tracker::ServiceData &service_data) { + ParseResult result; + if (!service_data.uuid.contains(0x1B, 0x18)) { + ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + return {}; + } + + return result; +} + +bool XiaomiMiscale2::parse_message(const std::vector &message, ParseResult &result) { + // 2-3 Years (MISCALE 2 181B) + // 4 month (MISCALE 2 181B) + // 5 day (MISCALE 2 181B) + // 6 hour (MISCALE 2 181B) + // 7 minute (MISCALE 2 181B) + // 8 second (MISCALE 2 181B) + // 9-10 impedance (MISCALE 2 181B) + // 11-12 weight (MISCALE 2 181B) + + const uint8_t *data = message.data(); + const int data_length = 13; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + bool is_Stabilized = ((data[1] & (1 << 5)) != 0) ? true : false; + bool loadRemoved = ((data[1] & (1 << 7)) != 0) ? true : false; + + // weight, 2 bytes, 16-bit unsigned integer, 1 kg + const int16_t weight = uint16_t(data[11]) | (uint16_t(data[12]) << 8); + if (data[0] == 0x02) + result.weight = weight * 0.01f / 2.0f; // unit 'kg' + else if (data[0] == 0x03) + result.weight = weight * 0.01f * 0.453592; // unit 'lbs' + + // impedance, 2 bytes, 16-bit + const int16_t impedance = uint16_t(data[9]) | (uint16_t(data[10]) << 8); + result.impedance = impedance; + + if (!is_Stabilized || loadRemoved || impedance == 0 || impedance >= 3000) { + return false; + } + + return true; +} + +bool XiaomiMiscale2::report_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got Xiaomi Miscale2 (%s):", address.c_str()); + + if (result->weight.has_value()) { + ESP_LOGD(TAG, " Weight: %.2fkg", *result->weight); + } + if (result->impedance.has_value()) { + ESP_LOGD(TAG, " Impedance: %.0fohm", *result->impedance); + } + + return true; +} + +} // namespace xiaomi_miscale2 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h new file mode 100644 index 0000000000..ead522e1f2 --- /dev/null +++ b/esphome/components/xiaomi_miscale2/xiaomi_miscale2.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miscale2 { + +struct ParseResult { + optional weight; + optional impedance; +}; + +class XiaomiMiscale2 : 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; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_weight(sensor::Sensor *weight) { weight_ = weight; } + void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } + + protected: + uint64_t address_; + sensor::Sensor *weight_{nullptr}; + sensor::Sensor *impedance_{nullptr}; + + optional parse_header(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message(const std::vector &message, ParseResult &result); + bool report_results(const optional &result, const std::string &address); +}; + +} // namespace xiaomi_miscale2 +} // namespace esphome + +#endif diff --git a/esphome/const.py b/esphome/const.py index 0eeec36a54..345cac89dd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -244,6 +244,7 @@ CONF_IDLE_TIME = 'idle_time' CONF_IF = 'if' CONF_IIR_FILTER = 'iir_filter' CONF_ILLUMINANCE = 'illuminance' +CONF_IMPEDANCE = 'impedance' CONF_INCLUDES = 'includes' CONF_INDEX = 'index' CONF_INDOOR = 'indoor' @@ -565,6 +566,7 @@ CONF_WAKEUP_PIN = 'wakeup_pin' CONF_WARM_WHITE = 'warm_white' CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' CONF_WATCHDOG_THRESHOLD = 'watchdog_threshold' +CONF_WEIGHT = 'weight' CONF_WHILE = 'while' CONF_WHITE = 'white' CONF_WIDTH = 'width' @@ -602,6 +604,7 @@ ICON_MAGNET = 'mdi:magnet' ICON_MOLECULE_CO2 = 'mdi:molecule-co2' ICON_MOTION_SENSOR = 'mdi:motion-sensor' ICON_NEW_BOX = 'mdi:new-box' +ICON_OMEGA = 'mdi:omega' ICON_PERCENT = 'mdi:percent' ICON_POWER = 'mdi:power' ICON_PULSE = 'mdi:pulse' @@ -610,6 +613,7 @@ ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_RULER = 'mdi:ruler' ICON_SCALE = 'mdi:scale' +ICON_SCALE_BATHROOM = 'mdi:scale-bathroom' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGN_DIRECTION = 'mdi:sign-direction' ICON_SIGNAL = 'mdi:signal-distance-variant' @@ -636,6 +640,7 @@ UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' UNIT_HERTZ = 'Hz' UNIT_KELVIN = 'K' +UNIT_KILOGRAM = 'kg' UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' UNIT_LUX = 'lx'