mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	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 <you@example.com>
This commit is contained in:
		| @@ -161,8 +161,9 @@ esphome/components/modbus_controller/select/* @martgras @stegm | |||||||
| esphome/components/modbus_controller/sensor/* @martgras | esphome/components/modbus_controller/sensor/* @martgras | ||||||
| esphome/components/modbus_controller/switch/* @martgras | esphome/components/modbus_controller/switch/* @martgras | ||||||
| esphome/components/modbus_controller/text_sensor/* @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_pro_check/* @spbrogan | ||||||
|  | esphome/components/mopeka_std_check/* @Fabian-Schmidt | ||||||
| esphome/components/mpl3115a2/* @kbickar | esphome/components/mpl3115a2/* @kbickar | ||||||
| esphome/components/mpu6886/* @fabaff | esphome/components/mpu6886/* @fabaff | ||||||
| esphome/components/network/* @esphome/core | esphome/components/network/* @esphome/core | ||||||
|   | |||||||
| @@ -3,9 +3,11 @@ import esphome.config_validation as cv | |||||||
| from esphome.components import esp32_ble_tracker | from esphome.components import esp32_ble_tracker | ||||||
| from esphome.const import CONF_ID | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
| CODEOWNERS = ["@spbrogan"] | CODEOWNERS = ["@spbrogan", "@Fabian-Schmidt"] | ||||||
| DEPENDENCIES = ["esp32_ble_tracker"] | DEPENDENCIES = ["esp32_ble_tracker"] | ||||||
|  |  | ||||||
|  | CONF_SHOW_SENSORS_WITHOUT_SYNC = "show_sensors_without_sync" | ||||||
|  |  | ||||||
| mopeka_ble_ns = cg.esphome_ns.namespace("mopeka_ble") | mopeka_ble_ns = cg.esphome_ns.namespace("mopeka_ble") | ||||||
| MopekaListener = mopeka_ble_ns.class_( | MopekaListener = mopeka_ble_ns.class_( | ||||||
|     "MopekaListener", esp32_ble_tracker.ESPBTDeviceListener |     "MopekaListener", esp32_ble_tracker.ESPBTDeviceListener | ||||||
| @@ -14,10 +16,15 @@ MopekaListener = mopeka_ble_ns.class_( | |||||||
| CONFIG_SCHEMA = cv.Schema( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(MopekaListener), |         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) | ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     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) |     await esp32_ble_tracker.register_ble_device(var, config) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| #include "mopeka_ble.h" | #include "mopeka_ble.h" | ||||||
|  |  | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| @@ -7,43 +8,83 @@ namespace esphome { | |||||||
| namespace mopeka_ble { | namespace mopeka_ble { | ||||||
|  |  | ||||||
| static const char *const TAG = "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. |  * Parse all incoming BLE payloads to see if it is a Mopeka BLE advertisement. | ||||||
|  * Currently this supports the following products: |  * Currently this supports the following products: | ||||||
|  * |  * | ||||||
|  *   Mopeka Pro Check. |  *  - Mopeka Std Check - uses the chip CC2540 by Texas Instruments (TI) | ||||||
|  *    If the sync button is pressed, report the MAC so a user can add this as a sensor. |  *  - 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) { | 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) { |   if (manu_datas.size() != 1) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const auto &manu_data = manu_datas[0]; |   const auto &manu_data = manu_datas[0]; | ||||||
|  |  | ||||||
|   if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { |   // Is the device maybe a Mopeka Std (CC2540) sensor. | ||||||
|     return false; |   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; |   return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool MopekaListener::parse_sync_button_(const std::vector<uint8_t> &message) { return (message[2] & 0x80) != 0; } |  | ||||||
|  |  | ||||||
| }  // namespace mopeka_ble | }  // namespace mopeka_ble | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" |  | ||||||
|  |  | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
|  | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -13,9 +13,12 @@ namespace mopeka_ble { | |||||||
| class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { | class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { | ||||||
|  public: |  public: | ||||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; |   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: |  protected: | ||||||
|   bool parse_sync_button_(const std::vector<uint8_t> &message); |   bool show_sensors_without_sync_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace mopeka_ble | }  // namespace mopeka_ble | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/mopeka_std_check/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/mopeka_std_check/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@Fabian-Schmidt"] | ||||||
							
								
								
									
										226
									
								
								esphome/components/mopeka_std_check/mopeka_std_check.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								esphome/components/mopeka_std_check/mopeka_std_check.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<SensorType>(hardware_id) != STANDARD && static_cast<SensorType>(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<u_int8_t, 12> measurements_time = {}; | ||||||
|  |     std::array<u_int8_t, 12> 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 | ||||||
							
								
								
									
										78
									
								
								esphome/components/mopeka_std_check/mopeka_std_check.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/mopeka_std_check/mopeka_std_check.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | #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 | ||||||
							
								
								
									
										139
									
								
								esphome/components/mopeka_std_check/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								esphome/components/mopeka_std_check/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||||
		Reference in New Issue
	
	Block a user