mirror of
https://github.com/esphome/esphome.git
synced 2024-11-10 01:07:45 +01: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:
parent
8fb481751f
commit
d16eff5039
8 changed files with 520 additions and 24 deletions
|
@ -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))
|
Loading…
Reference in a new issue