From d16eff5039a7503cc8274464290c7a0f1eec47b2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 21 Feb 2023 22:48:29 +0100 Subject: [PATCH] Support Mopeka Standard LPG tank bluetooth sensor (#4351) * Add mopeka standard tank sensor. * Enhance mopeka ble to find standard sensors. * Updated `CODEOWNERS` file * Move default from cpp to py. * Format documents with esphome settings. * Linter wants changes. * Update name of `get_lpg_speed_of_sound`. * manually update `CODEOWNERS`. * Manually update CODEOWNER, because `build_codeowners.py. is failing. * Add comments. * Use percentage for `propane_butane_mix`. * add config to `dump_config()` * Formatting * Use struct for data parsing and find best data. * Add `this`. * Consistant naming of configuration. * Fix format issues. * Make clang-tidy happy. * Adjust loop variable. --------- Co-authored-by: Your Name --- CODEOWNERS | 3 +- esphome/components/mopeka_ble/__init__.py | 9 +- esphome/components/mopeka_ble/mopeka_ble.cpp | 77 ++++-- esphome/components/mopeka_ble/mopeka_ble.h | 11 +- .../components/mopeka_std_check/__init__.py | 1 + .../mopeka_std_check/mopeka_std_check.cpp | 226 ++++++++++++++++++ .../mopeka_std_check/mopeka_std_check.h | 78 ++++++ esphome/components/mopeka_std_check/sensor.py | 139 +++++++++++ 8 files changed, 520 insertions(+), 24 deletions(-) create mode 100644 esphome/components/mopeka_std_check/__init__.py create mode 100644 esphome/components/mopeka_std_check/mopeka_std_check.cpp create mode 100644 esphome/components/mopeka_std_check/mopeka_std_check.h create mode 100644 esphome/components/mopeka_std_check/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9cfa81b36c..79c9c4f94b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -161,8 +161,9 @@ esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras -esphome/components/mopeka_ble/* @spbrogan +esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan esphome/components/mopeka_pro_check/* @spbrogan +esphome/components/mopeka_std_check/* @Fabian-Schmidt esphome/components/mpl3115a2/* @kbickar esphome/components/mpu6886/* @fabaff esphome/components/network/* @esphome/core diff --git a/esphome/components/mopeka_ble/__init__.py b/esphome/components/mopeka_ble/__init__.py index 47396435a8..c89eae7933 100644 --- a/esphome/components/mopeka_ble/__init__.py +++ b/esphome/components/mopeka_ble/__init__.py @@ -3,9 +3,11 @@ import esphome.config_validation as cv from esphome.components import esp32_ble_tracker from esphome.const import CONF_ID -CODEOWNERS = ["@spbrogan"] +CODEOWNERS = ["@spbrogan", "@Fabian-Schmidt"] DEPENDENCIES = ["esp32_ble_tracker"] +CONF_SHOW_SENSORS_WITHOUT_SYNC = "show_sensors_without_sync" + mopeka_ble_ns = cg.esphome_ns.namespace("mopeka_ble") MopekaListener = mopeka_ble_ns.class_( "MopekaListener", esp32_ble_tracker.ESPBTDeviceListener @@ -14,10 +16,15 @@ MopekaListener = mopeka_ble_ns.class_( CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(MopekaListener), + cv.Optional(CONF_SHOW_SENSORS_WITHOUT_SYNC, default=False): cv.boolean, } ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + if CONF_SHOW_SENSORS_WITHOUT_SYNC in config: + cg.add( + var.set_show_sensors_without_sync(config[CONF_SHOW_SENSORS_WITHOUT_SYNC]) + ) await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/mopeka_ble/mopeka_ble.cpp b/esphome/components/mopeka_ble/mopeka_ble.cpp index 844d3a7dfd..07c8ac5d71 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.cpp +++ b/esphome/components/mopeka_ble/mopeka_ble.cpp @@ -1,4 +1,5 @@ #include "mopeka_ble.h" + #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -7,43 +8,83 @@ namespace esphome { namespace mopeka_ble { static const char *const TAG = "mopeka_ble"; -static const uint8_t MANUFACTURER_DATA_LENGTH = 10; -static const uint16_t MANUFACTURER_ID = 0x0059; + +// Mopeka Std (CC2540) sensor details +static const uint16_t SERVICE_UUID_CC2540 = 0xADA0; +static const uint16_t MANUFACTURER_CC2540_ID = 0x000D; // Texas Instruments (TI) +static const uint8_t MANUFACTURER_CC2540_DATA_LENGTH = 23; + +// Mopeka Pro (NRF52) sensor details +static const uint16_t SERVICE_UUID_NRF52 = 0xFEE5; +static const uint16_t MANUFACTURER_NRF52_ID = 0x0059; // Nordic +static const uint8_t MANUFACTURER_NRF52_DATA_LENGTH = 10; /** * Parse all incoming BLE payloads to see if it is a Mopeka BLE advertisement. * Currently this supports the following products: * - * Mopeka Pro Check. - * If the sync button is pressed, report the MAC so a user can add this as a sensor. + * - Mopeka Std Check - uses the chip CC2540 by Texas Instruments (TI) + * - Mopeka Pro Check - uses the chip NRF52 by Nordic + * + * If the sync button is pressed, report the MAC so a user can add this as a sensor. Or if user has configured + * `show_sensors_without_sync_` than report all visible sensors. + * Three points are used to identify a sensor: + * + * - Bluetooth service uuid + * - Bluetooth manufacturer id + * - Bluetooth data frame size */ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - const auto &manu_datas = device.get_manufacturer_datas(); + // Fetch information about BLE device. + const auto &service_uuids = device.get_service_uuids(); + if (service_uuids.size() != 1) { + return false; + } + const auto &service_uuid = service_uuids[0]; + const auto &manu_datas = device.get_manufacturer_datas(); if (manu_datas.size() != 1) { return false; } - const auto &manu_data = manu_datas[0]; - if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { - return false; + // Is the device maybe a Mopeka Std (CC2540) sensor. + if (service_uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID_CC2540)) { + if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_CC2540_ID)) { + return false; + } + + if (manu_data.data.size() != MANUFACTURER_CC2540_DATA_LENGTH) { + return false; + } + + const bool sync_button_pressed = (manu_data.data[3] & 0x80) != 0; + + if (this->show_sensors_without_sync_ || sync_button_pressed) { + ESP_LOGI(TAG, "MOPEKA STD (CC2540) SENSOR FOUND: %s", device.address_str().c_str()); + } + + // Is the device maybe a Mopeka Pro (NRF52) sensor. + } else if (service_uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID_NRF52)) { + if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_NRF52_ID)) { + return false; + } + + if (manu_data.data.size() != MANUFACTURER_NRF52_DATA_LENGTH) { + return false; + } + + const bool sync_button_pressed = (manu_data.data[2] & 0x80) != 0; + + if (this->show_sensors_without_sync_ || sync_button_pressed) { + ESP_LOGI(TAG, "MOPEKA PRO (NRF52) SENSOR FOUND: %s", device.address_str().c_str()); + } } - if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_ID)) { - return false; - } - - if (this->parse_sync_button_(manu_data.data)) { - // button pressed - ESP_LOGI(TAG, "SENSOR FOUND: %s", device.address_str().c_str()); - } return false; } -bool MopekaListener::parse_sync_button_(const std::vector &message) { return (message[2] & 0x80) != 0; } - } // namespace mopeka_ble } // namespace esphome diff --git a/esphome/components/mopeka_ble/mopeka_ble.h b/esphome/components/mopeka_ble/mopeka_ble.h index f88bad4f3a..b7d0c5a9c5 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.h +++ b/esphome/components/mopeka_ble/mopeka_ble.h @@ -1,10 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" - #include +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" + #ifdef USE_ESP32 namespace esphome { @@ -13,9 +13,12 @@ namespace mopeka_ble { class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void set_show_sensors_without_sync(bool show_sensors_without_sync) { + show_sensors_without_sync_ = show_sensors_without_sync; + } protected: - bool parse_sync_button_(const std::vector &message); + bool show_sensors_without_sync_; }; } // namespace mopeka_ble diff --git a/esphome/components/mopeka_std_check/__init__.py b/esphome/components/mopeka_std_check/__init__.py new file mode 100644 index 0000000000..88e344464f --- /dev/null +++ b/esphome/components/mopeka_std_check/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Fabian-Schmidt"] diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp new file mode 100644 index 0000000000..cbe51b8f2d --- /dev/null +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -0,0 +1,226 @@ +#include "mopeka_std_check.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_std_check { + +static const char *const TAG = "mopeka_std_check"; +static const uint16_t SERVICE_UUID = 0xADA0; +static const uint8_t MANUFACTURER_DATA_LENGTH = 23; +static const uint16_t MANUFACTURER_ID = 0x000D; + +void MopekaStdCheck::dump_config() { + ESP_LOGCONFIG(TAG, "Mopeka Std Check"); + ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100); + ESP_LOGCONFIG(TAG, " Tank distance empty: %imm", this->empty_mm_); + ESP_LOGCONFIG(TAG, " Tank distance full: %imm", this->full_mm_); + LOG_SENSOR(" ", "Level", this->level_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Reading Distance", this->distance_); +} + +/** + * Main parse function that gets called for all ble advertisements. + * Check if advertisement is for our sensor and if so decode it and + * update the sensor state data. + */ +bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + { + // Validate address. + if (device.address_uint64() != this->address_) { + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + } + + { + // Validate service uuid + const auto &service_uuids = device.get_service_uuids(); + if (service_uuids.size() != 1) { + return false; + } + const auto &service_uuid = service_uuids[0]; + if (service_uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID)) { + return false; + } + } + + const auto &manu_datas = device.get_manufacturer_datas(); + + if (manu_datas.size() != 1) { + ESP_LOGE(TAG, "%s: Unexpected manu_datas size (%d)", device.address_str().c_str(), manu_datas.size()); + return false; + } + + const auto &manu_data = manu_datas[0]; + + ESP_LOGVV(TAG, "%s: Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); + + if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { + ESP_LOGE(TAG, "%s: Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); + return false; + } + + // Now parse the data + const auto *mopeka_data = (const mopeka_std_package *) manu_data.data.data(); + + const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; + if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL) { + ESP_LOGE(TAG, "%s: Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); + return false; + } + + ESP_LOGVV(TAG, "%s: Sensor slow update rate: %d", device.address_str().c_str(), mopeka_data->slow_update_rate); + ESP_LOGVV(TAG, "%s: Sensor sync pressed: %d", device.address_str().c_str(), mopeka_data->sync_pressed); + for (u_int8_t i = 0; i < 3; i++) { + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 1, + mopeka_data->val[i].value_0, mopeka_data->val[i].time_0); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 2, + mopeka_data->val[i].value_1, mopeka_data->val[i].time_1); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 3, + mopeka_data->val[i].value_2, mopeka_data->val[i].time_2); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 4, + mopeka_data->val[i].value_3, mopeka_data->val[i].time_3); + } + + // Get battery level first + if (this->battery_level_ != nullptr) { + uint8_t level = this->parse_battery_level_(mopeka_data); + this->battery_level_->publish_state(level); + } + + // Get temperature of sensor + uint8_t temp_in_c = this->parse_temperature_(mopeka_data); + if (this->temperature_ != nullptr) { + this->temperature_->publish_state(temp_in_c); + } + + // Get distance and level if either are sensors + if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { + // Message contains 12 sensor dataset each 10 bytes long. + // each sensor dataset contains 5 byte time and 5 byte value. + + // time in 10us ticks. + // value is amplitude. + + std::array measurements_time = {}; + std::array measurements_value = {}; + // Copy measurements over into my array. + { + u_int8_t measurements_index = 0; + for (u_int8_t i = 0; i < 3; i++) { + measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_0; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_1; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_2; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_3; + measurements_index++; + } + } + + // Find best(strongest) value(amplitude) and it's belonging time in sensor dataset. + u_int8_t number_of_usable_values = 0; + u_int16_t best_value = 0; + u_int16_t best_time = 0; + { + u_int16_t measurement_time = 0; + for (u_int8_t i = 0; i < 12; i++) { + // Time is summed up until a value is reported. This allows time values larger than the 5 bits in transport. + measurement_time += measurements_time[i]; + if (measurements_value[i] != 0) { + // I got a value + number_of_usable_values++; + if (measurements_value[i] > best_value) { + // This value is better than a previous one. + best_value = measurements_value[i]; + best_time = measurement_time; + // Reset measurement_time or next values. + measurement_time = 0; + } + } + } + } + + ESP_LOGV(TAG, "%s: Found %u values with best data %u time %u.", device.address_str().c_str(), + number_of_usable_values, best_value, best_time); + + if (number_of_usable_values < 2 || best_value < 2 || best_time < 2) { + // At least two measurement values must be present. + ESP_LOGW(TAG, "%s: Poor read quality. Setting distance to 0.", device.address_str().c_str()); + if (this->distance_ != nullptr) { + this->distance_->publish_state(0); + } + if (this->level_ != nullptr) { + this->level_->publish_state(0); + } + } else { + float lpg_speed_of_sound = this->get_lpg_speed_of_sound_(temp_in_c); + ESP_LOGV(TAG, "%s: Speed of sound in current fluid %f m/s", device.address_str().c_str(), lpg_speed_of_sound); + + uint32_t distance_value = lpg_speed_of_sound * best_time / 100.0f; + + // update distance sensor + if (this->distance_ != nullptr) { + this->distance_->publish_state(distance_value); + } + + // update level sensor + if (this->level_ != nullptr) { + uint8_t tank_level = 0; + if (distance_value >= this->full_mm_) { + tank_level = 100; // cap at 100% + } else if (distance_value > this->empty_mm_) { + tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_)); + } + this->level_->publish_state(tank_level); + } + } + } + + return true; +} + +float MopekaStdCheck::get_lpg_speed_of_sound_(float temperature) { + return 1040.71f - 4.87f * temperature - 137.5f * this->propane_butane_mix_ - 0.0107f * temperature * temperature - + 1.63f * temperature * this->propane_butane_mix_; +} + +uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message) { + const float voltage = (float) ((message->raw_voltage / 256.0f) * 2.0f + 1.5f); + ESP_LOGVV(TAG, "Sensor battery voltage: %f V", voltage); + // convert voltage and scale for CR2032 + const float percent = (voltage - 2.2f) / 0.65f * 100.0f; + if (percent < 0.0f) { + return 0; + } + if (percent > 100.0f) { + return 100; + } + return (uint8_t) percent; +} + +uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { + uint8_t tmp = message->raw_temp; + if (tmp == 0x0) { + return -40; + } else { + return (uint8_t)((tmp - 25.0f) * 1.776964f); + } +} + +} // namespace mopeka_std_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h new file mode 100644 index 0000000000..e4d81afbd7 --- /dev/null +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_std_check { + +enum SensorType { + STANDARD = 0x02, + XL = 0x03, +}; + +// 4 values in one struct so it aligns to 8 byte. One `mopeka_std_values` is 40 bit long. +struct mopeka_std_values { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + u_int16_t time_0 : 5; + u_int16_t value_0 : 5; + u_int16_t time_1 : 5; + u_int16_t value_1 : 5; + u_int16_t time_2 : 5; + u_int16_t value_2 : 5; + u_int16_t time_3 : 5; + u_int16_t value_3 : 5; +} __attribute__((packed)); + +struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + u_int8_t data_0 : 8; + u_int8_t data_1 : 8; + u_int8_t raw_voltage : 8; + + u_int8_t raw_temp : 6; + bool slow_update_rate : 1; + bool sync_pressed : 1; + + mopeka_std_values val[4]; +} __attribute__((packed)); + +class MopekaStdCheck : 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_level(sensor::Sensor *level) { this->level_ = level; }; + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; + void set_battery_level(sensor::Sensor *bat) { this->battery_level_ = bat; }; + void set_distance(sensor::Sensor *distance) { this->distance_ = distance; }; + void set_propane_butane_mix(float val) { this->propane_butane_mix_ = val; }; + void set_tank_full(float full) { this->full_mm_ = full; }; + void set_tank_empty(float empty) { this->empty_mm_ = empty; }; + + protected: + uint64_t address_; + sensor::Sensor *level_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *distance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + float propane_butane_mix_; + uint32_t full_mm_; + uint32_t empty_mm_; + + float get_lpg_speed_of_sound_(float temperature); + uint8_t parse_battery_level_(const mopeka_std_package *message); + uint8_t parse_temperature_(const mopeka_std_package *message); +}; + +} // namespace mopeka_std_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_std_check/sensor.py b/esphome/components/mopeka_std_check/sensor.py new file mode 100644 index 0000000000..bbba798e95 --- /dev/null +++ b/esphome/components/mopeka_std_check/sensor.py @@ -0,0 +1,139 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_DISTANCE, + CONF_MAC_ADDRESS, + CONF_ID, + ICON_THERMOMETER, + ICON_RULER, + UNIT_PERCENT, + CONF_LEVEL, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + STATE_CLASS_MEASUREMENT, + CONF_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, +) + +CONF_TANK_TYPE = "tank_type" +CONF_CUSTOM_DISTANCE_FULL = "custom_distance_full" +CONF_CUSTOM_DISTANCE_EMPTY = "custom_distance_empty" +CONF_PROPANE_BUTANE_MIX = "propane_butane_mix" + +ICON_PROPANE_TANK = "mdi:propane-tank" + +TANK_TYPE_CUSTOM = "CUSTOM" + +UNIT_MILLIMETER = "mm" + + +def small_distance(value): + """small_distance is stored in mm""" + meters = cv.distance(value) + return meters * 1000 + + +# +# Map of standard tank types to their +# empty and full distance values. +# Format is - tank name: (empty distance in mm, full distance in mm) +# +CONF_SUPPORTED_TANKS_MAP = { + TANK_TYPE_CUSTOM: (38, 100), + "NORTH_AMERICA_20LB_VERTICAL": (38, 254), # empty/full readings for 20lb US tank + "NORTH_AMERICA_30LB_VERTICAL": (38, 381), + "NORTH_AMERICA_40LB_VERTICAL": (38, 508), + "EUROPE_6KG": (38, 336), + "EUROPE_11KG": (38, 366), + "EUROPE_14KG": (38, 467), +} + +CODEOWNERS = ["@Fabian-Schmidt"] +DEPENDENCIES = ["esp32_ble_tracker"] + +mopeka_std_check_ns = cg.esphome_ns.namespace("mopeka_std_check") +MopekaStdCheck = mopeka_std_check_ns.class_( + "MopekaStdCheck", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MopekaStdCheck), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance, + cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance, + cv.Optional(CONF_PROPANE_BUTANE_MIX, default="100%"): cv.percentage, + cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PROPANE_TANK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if config[CONF_TANK_TYPE] == TANK_TYPE_CUSTOM: + # Support custom tank min/max + if CONF_CUSTOM_DISTANCE_EMPTY in config: + cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY])) + else: + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0])) + if CONF_CUSTOM_DISTANCE_FULL in config: + cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL])) + else: + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1])) + else: + # Set the Tank empty and full based on map - User is requesting standard tank + t = config[CONF_TANK_TYPE] + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0])) + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1])) + + if CONF_PROPANE_BUTANE_MIX in config: + cg.add(var.set_propane_butane_mix(config[CONF_PROPANE_BUTANE_MIX])) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_LEVEL]) + cg.add(var.set_level(sens)) + if CONF_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_DISTANCE]) + cg.add(var.set_distance(sens)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens))