diff --git a/CODEOWNERS b/CODEOWNERS index a77a7ba86e..acf8acb2ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva -esphome/components/bl0940/* @tobias- +esphome/components/bl0940/* @dan-s-github @tobias- esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @bdraco @jesserockz diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py index 087626a4e7..066c2818b6 100644 --- a/esphome/components/bl0940/__init__.py +++ b/esphome/components/bl0940/__init__.py @@ -1 +1,6 @@ -CODEOWNERS = ["@tobias-"] +import esphome.codegen as cg + +CODEOWNERS = ["@tobias-", "@dan-s-github"] + +CONF_BL0940_ID = "bl0940_id" +bl0940_ns = cg.esphome_ns.namespace("bl0940") diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 24990d5482..42e20eb69b 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -7,28 +7,26 @@ namespace bl0940 { static const char *const TAG = "bl0940"; -static const uint8_t BL0940_READ_COMMAND = 0x50; // 0x58 according to documentation static const uint8_t BL0940_FULL_PACKET = 0xAA; -static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to documentation +static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to en doc but 0x55 in cn doc -static const uint8_t BL0940_WRITE_COMMAND = 0xA0; // 0xA8 according to documentation static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; static const uint8_t BL0940_REG_MODE = 0x18; static const uint8_t BL0940_REG_SOFT_RESET = 0x19; static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; -const uint8_t BL0940_INIT[5][6] = { +static const uint8_t BL0940_INIT[5][5] = { // Reset to default - {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, // Enable User Operation Write - {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS - {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS - {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, // 0x181C = Half cycle, Fast RMS threshold 6172 - {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; void BL0940::loop() { DataPacket buffer; @@ -36,8 +34,8 @@ void BL0940::loop() { return; } if (read_array((uint8_t *) &buffer, sizeof(buffer))) { - if (validate_checksum(&buffer)) { - received_package_(&buffer); + if (this->validate_checksum_(&buffer)) { + this->received_package_(&buffer); } } else { ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); @@ -46,35 +44,151 @@ void BL0940::loop() { } } -bool BL0940::validate_checksum(const DataPacket *data) { - uint8_t checksum = BL0940_READ_COMMAND; +bool BL0940::validate_checksum_(DataPacket *data) { + uint8_t checksum = this->read_command_; // Whole package but checksum - for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { - checksum += data->raw[i]; + uint8_t *raw = (uint8_t *) data; + for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { + checksum += raw[i]; } checksum ^= 0xFF; if (checksum != data->checksum) { - ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); } return checksum == data->checksum; } void BL0940::update() { this->flush(); - this->write_byte(BL0940_READ_COMMAND); + this->write_byte(this->read_command_); this->write_byte(BL0940_FULL_PACKET); } void BL0940::setup() { +#ifdef USE_NUMBER + // add calibration callbacks + if (this->voltage_calibration_number_ != nullptr) { + this->voltage_calibration_number_->add_on_state_callback( + [this](float state) { this->voltage_calibration_callback_(state); }); + if (this->voltage_calibration_number_->has_state()) { + this->voltage_calibration_callback_(this->voltage_calibration_number_->state); + } + } + + if (this->current_calibration_number_ != nullptr) { + this->current_calibration_number_->add_on_state_callback( + [this](float state) { this->current_calibration_callback_(state); }); + if (this->current_calibration_number_->has_state()) { + this->current_calibration_callback_(this->current_calibration_number_->state); + } + } + + if (this->power_calibration_number_ != nullptr) { + this->power_calibration_number_->add_on_state_callback( + [this](float state) { this->power_calibration_callback_(state); }); + if (this->power_calibration_number_->has_state()) { + this->power_calibration_callback_(this->power_calibration_number_->state); + } + } + + if (this->energy_calibration_number_ != nullptr) { + this->energy_calibration_number_->add_on_state_callback( + [this](float state) { this->energy_calibration_callback_(state); }); + if (this->energy_calibration_number_->has_state()) { + this->energy_calibration_callback_(this->energy_calibration_number_->state); + } + } +#endif + + // calculate calibrated reference values + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + for (auto *i : BL0940_INIT) { - this->write_array(i, 6); + this->write_byte(this->write_command_), this->write_array(i, 5); delay(1); } this->flush(); } -float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { - auto tb = (float) (temperature.h << 8 | temperature.l); +float BL0940::calculate_power_reference_() { + // calculate power reference based on voltage and current reference + return this->voltage_reference_cal_ * this->current_reference_cal_ * 4046 / 324004 / 79931; +} + +float BL0940::calculate_energy_reference_() { + // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) + // or: power_reference_ * 3600000 / (1638.4 * 256) + return this->power_reference_cal_ * 3600000 / (1638.4 * 256); +} + +float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } + +void BL0940::reset_calibration() { +#ifdef USE_NUMBER + if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) { + this->current_calibration_number_->make_call().set_value(0).perform(); + } + if (this->voltage_calibration_number_ != nullptr && this->voltage_cal_ != 1) { + this->voltage_calibration_number_->make_call().set_value(0).perform(); + } + if (this->power_calibration_number_ != nullptr && this->power_cal_ != 1) { + this->power_calibration_number_->make_call().set_value(0).perform(); + } + if (this->energy_calibration_number_ != nullptr && this->energy_cal_ != 1) { + this->energy_calibration_number_->make_call().set_value(0).perform(); + } +#endif + ESP_LOGD(TAG, "external calibration values restored to initial state"); +} + +void BL0940::current_calibration_callback_(float state) { + this->current_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update current calibration state: %f", this->current_cal_); + this->recalibrate_(); +} +void BL0940::voltage_calibration_callback_(float state) { + this->voltage_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update voltage calibration state: %f", this->voltage_cal_); + this->recalibrate_(); +} +void BL0940::power_calibration_callback_(float state) { + this->power_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update power calibration state: %f", this->power_cal_); + this->recalibrate_(); +} +void BL0940::energy_calibration_callback_(float state) { + this->energy_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update energy calibration state: %f", this->energy_cal_); + this->recalibrate_(); +} + +void BL0940::recalibrate_() { + ESP_LOGV(TAG, "Recalibrating reference values"); + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1) { + this->power_reference_ = this->calculate_power_reference_(); + } + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1 || this->power_cal_ != 1) { + this->energy_reference_ = this->calculate_energy_reference_(); + } + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + + ESP_LOGD(TAG, + "Recalibrated reference values:\n" + "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, + this->energy_reference_cal_); +} + +float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) const { + auto tb = (float) temperature; float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; if (sensor != nullptr) { if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { @@ -87,33 +201,40 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { return converted_temp; } -void BL0940::received_package_(const DataPacket *data) const { +void BL0940::received_package_(DataPacket *data) { // Bad header if (data->frame_header != BL0940_PACKET_HEADER) { ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); return; } - float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; - float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; - float watt = (float) to_int32_t(data->watt) / power_reference_; - uint32_t cf_cnt = to_uint32_t(data->cf_cnt); - float total_energy_consumption = (float) cf_cnt / energy_reference_; + // cf_cnt is only 24 bits, so track overflows + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + cf_cnt |= this->prev_cf_cnt_ & 0xff000000; + if (cf_cnt < this->prev_cf_cnt_) { + cf_cnt += 0x1000000; + } + this->prev_cf_cnt_ = cf_cnt; - float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); - float tps2 = update_temp_(external_temperature_sensor_, data->tps2); + float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; + float i_rms = (uint24_t) data->i_rms / this->current_reference_cal_; + float watt = (int24_t) data->watt / this->power_reference_cal_; + float total_energy_consumption = cf_cnt / this->energy_reference_cal_; - if (voltage_sensor_ != nullptr) { - voltage_sensor_->publish_state(v_rms); + float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); + float tps2 = update_temp_(this->external_temperature_sensor_, data->tps2); + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(v_rms); } - if (current_sensor_ != nullptr) { - current_sensor_->publish_state(i_rms); + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(i_rms); } - if (power_sensor_ != nullptr) { - power_sensor_->publish_state(watt); + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(watt); } - if (energy_sensor_ != nullptr) { - energy_sensor_->publish_state(total_energy_consumption); + if (this->energy_sensor_ != nullptr) { + this->energy_sensor_->publish_state(total_energy_consumption); } ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { } void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexity) - ESP_LOGCONFIG(TAG, "BL0940:"); + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " LEGACY MODE: %s\n" + " READ CMD: 0x%02X\n" + " WRITE CMD: 0x%02X\n" + " ------------------\n" + " Current reference: %f\n" + " Energy reference: %f\n" + " Power reference: %f\n" + " Voltage reference: %f\n", + TRUEFALSE(this->legacy_mode_enabled_), this->read_command_, this->write_command_, + this->current_reference_, this->energy_reference_, this->power_reference_, this->voltage_reference_); +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " Current calibration: %f\n" + " Energy calibration: %f\n" + " Power calibration: %f\n" + " Voltage calibration: %f\n", + this->current_cal_, this->energy_cal_, this->power_cal_, this->voltage_cal_); +#endif LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); @@ -130,9 +271,5 @@ void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexit LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); } -uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } - -int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } - } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 2d4e7ccaac..93d54003f5 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -1,66 +1,48 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/uart/uart.h" +#include "esphome/core/datatypes.h" +#include "esphome/core/defines.h" +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif #include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" namespace esphome { namespace bl0940 { -static const float BL0940_PREF = 1430; -static const float BL0940_UREF = 33000; -static const float BL0940_IREF = 275000; // 2750 from tasmota. Seems to generate values 100 times too high - -// Measured to 297J per click according to power consumption of 5 minutes -// Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 -static const float BL0940_EREF = 3.6e6 / 297; - -struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - uint8_t h; -} __attribute__((packed)); - -struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t h; -} __attribute__((packed)); - -struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - int8_t h; -} __attribute__((packed)); - // Caveat: All these values are big endian (low - middle - high) - -union DataPacket { // NOLINT(altera-struct-pack-align) - uint8_t raw[35]; - struct { - uint8_t frame_header; // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. - ube24_t i_fast_rms; // 0x00 - ube24_t i_rms; // 0x04 - ube24_t RESERVED0; // reserved - ube24_t v_rms; // 0x06 - ube24_t RESERVED1; // reserved - sbe24_t watt; // 0x08 - ube24_t RESERVED2; // reserved - ube24_t cf_cnt; // 0x0A - ube24_t RESERVED3; // reserved - ube16_t tps1; // 0x0c - uint8_t RESERVED4; // value of 0x00 - ube16_t tps2; // 0x0c - uint8_t RESERVED5; // value of 0x00 - uint8_t checksum; // checksum - }; +struct DataPacket { + uint8_t frame_header; // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) + uint24_le_t i_fast_rms; // Fast RMS current + uint24_le_t i_rms; // RMS current + uint24_t RESERVED0; // Reserved + uint24_le_t v_rms; // RMS voltage + uint24_t RESERVED1; // Reserved + int24_le_t watt; // Active power (can be negative for bidirectional measurement) + uint24_t RESERVED2; // Reserved + uint24_le_t cf_cnt; // Energy pulse count + uint24_t RESERVED3; // Reserved + uint16_le_t tps1; // Internal temperature sensor 1 + uint8_t RESERVED4; // Reserved (should be 0x00) + uint16_le_t tps2; // Internal temperature sensor 2 + uint8_t RESERVED5; // Reserved (should be 0x00) + uint8_t checksum; // Packet checksum } __attribute__((packed)); class BL0940 : public PollingComponent, public uart::UARTDevice { public: + // Sensor setters void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + + // Temperature sensor setters void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { internal_temperature_sensor_ = internal_temperature_sensor; } @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { external_temperature_sensor_ = external_temperature_sensor; } - void loop() override; + // Configuration setters + void set_legacy_mode(bool enable) { this->legacy_mode_enabled_ = enable; } + void set_read_command(uint8_t read_command) { this->read_command_ = read_command; } + void set_write_command(uint8_t write_command) { this->write_command_ = write_command; } + // Reference value setters (used for calibration and conversion) + void set_current_reference(float current_ref) { this->current_reference_ = current_ref; } + void set_energy_reference(float energy_ref) { this->energy_reference_ = energy_ref; } + void set_power_reference(float power_ref) { this->power_reference_ = power_ref; } + void set_voltage_reference(float voltage_ref) { this->voltage_reference_ = voltage_ref; } + +#ifdef USE_NUMBER + // Calibration number setters (for Home Assistant number entities) + void set_current_calibration_number(number::Number *num) { this->current_calibration_number_ = num; } + void set_voltage_calibration_number(number::Number *num) { this->voltage_calibration_number_ = num; } + void set_power_calibration_number(number::Number *num) { this->power_calibration_number_ = num; } + void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } +#endif + +#ifdef USE_BUTTON + // Resets all calibration values to defaults (can be triggered by a button) + void reset_calibration(); +#endif + + // Core component methods + void loop() override; void update() override; void setup() override; void dump_config() override; protected: - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *current_sensor_{nullptr}; - // NB This may be negative as the circuits is seemingly able to measure - // power in both directions - sensor::Sensor *power_sensor_{nullptr}; - sensor::Sensor *energy_sensor_{nullptr}; - sensor::Sensor *internal_temperature_sensor_{nullptr}; - sensor::Sensor *external_temperature_sensor_{nullptr}; + // --- Sensor pointers --- + sensor::Sensor *voltage_sensor_{nullptr}; // Voltage sensor + sensor::Sensor *current_sensor_{nullptr}; // Current sensor + sensor::Sensor *power_sensor_{nullptr}; // Power sensor (can be negative for bidirectional) + sensor::Sensor *energy_sensor_{nullptr}; // Energy sensor + sensor::Sensor *internal_temperature_sensor_{nullptr}; // Internal temperature sensor + sensor::Sensor *external_temperature_sensor_{nullptr}; // External temperature sensor - // Max difference between two measurements of the temperature. Used to avoid noise. - float max_temperature_diff_{0}; - // Divide by this to turn into Watt - float power_reference_ = BL0940_PREF; - // Divide by this to turn into Volt - float voltage_reference_ = BL0940_UREF; - // Divide by this to turn into Ampere - float current_reference_ = BL0940_IREF; - // Divide by this to turn into kWh - float energy_reference_ = BL0940_EREF; +#ifdef USE_NUMBER + // --- Calibration number entities (for dynamic calibration via HA UI) --- + number::Number *voltage_calibration_number_{nullptr}; + number::Number *current_calibration_number_{nullptr}; + number::Number *power_calibration_number_{nullptr}; + number::Number *energy_calibration_number_{nullptr}; +#endif - float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; + // --- Internal state --- + uint32_t prev_cf_cnt_ = 0; // Previous energy pulse count (for energy calculation) + float max_temperature_diff_{0}; // Max allowed temperature difference between two measurements (noise filter) - static uint32_t to_uint32_t(ube24_t input); + // --- Reference values for conversion --- + float power_reference_; // Divider for raw power to get Watts + float power_reference_cal_; // Calibrated power reference + float voltage_reference_; // Divider for raw voltage to get Volts + float voltage_reference_cal_; // Calibrated voltage reference + float current_reference_; // Divider for raw current to get Amperes + float current_reference_cal_; // Calibrated current reference + float energy_reference_; // Divider for raw energy to get kWh + float energy_reference_cal_; // Calibrated energy reference - static int32_t to_int32_t(sbe24_t input); + // --- Home Assistant calibration values (multipliers, default 1) --- + float current_cal_{1}; + float voltage_cal_{1}; + float power_cal_{1}; + float energy_cal_{1}; - static bool validate_checksum(const DataPacket *data); + // --- Protocol commands --- + uint8_t read_command_; + uint8_t write_command_; - void received_package_(const DataPacket *data) const; + // --- Mode flags --- + bool legacy_mode_enabled_ = true; + + // --- Methods --- + // Converts packed temperature value to float and updates the sensor + float update_temp_(sensor::Sensor *sensor, uint16_le_t packed_temperature) const; + + // Validates the checksum of a received data packet + bool validate_checksum_(DataPacket *data); + + // Handles a received data packet + void received_package_(DataPacket *data); + + // Calculates reference values for calibration and conversion + float calculate_energy_reference_(); + float calculate_power_reference_(); + float calculate_calibration_value_(float state); + + // Calibration update callbacks (used with number entities) + void current_calibration_callback_(float state); + void voltage_calibration_callback_(float state); + void power_calibration_callback_(float state); + void energy_calibration_callback_(float state); + void reset_calibration_callback_(); + + // Recalculates all reference values after calibration changes + void recalibrate_(); }; + } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/button/__init__.py b/esphome/components/bl0940/button/__init__.py new file mode 100644 index 0000000000..04d11e6e30 --- /dev/null +++ b/esphome/components/bl0940/button/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +CalibrationResetButton = bl0940_ns.class_( + "CalibrationResetButton", button.Button, cg.Component +) + +CONFIG_SCHEMA = cv.All( + button.button_schema( + CalibrationResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART, + ) + .extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await button.new_button(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/button/calibration_reset_button.cpp b/esphome/components/bl0940/button/calibration_reset_button.cpp new file mode 100644 index 0000000000..79a6b872d8 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.cpp @@ -0,0 +1,20 @@ +#include "calibration_reset_button.h" +#include "../bl0940.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.button.calibration_reset"; + +void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); } + +void CalibrationResetButton::press_action() { + ESP_LOGI(TAG, "Resetting calibration defaults..."); + this->parent_->reset_calibration(); +} + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/button/calibration_reset_button.h b/esphome/components/bl0940/button/calibration_reset_button.h new file mode 100644 index 0000000000..6ea3b35cb4 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace bl0940 { + +class BL0940; // Forward declaration of BL0940 class + +class CalibrationResetButton : public button::Button, public Component, public Parented { + public: + void dump_config() override; + + void press_action() override; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py new file mode 100644 index 0000000000..a640c2ae08 --- /dev/null +++ b/esphome/components/bl0940/number/__init__.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_RESTORE_VALUE, + CONF_STEP, + ENTITY_CATEGORY_CONFIG, + UNIT_PERCENT, +) + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +# Define calibration types +CONF_CURRENT_CALIBRATION = "current_calibration" +CONF_VOLTAGE_CALIBRATION = "voltage_calibration" +CONF_POWER_CALIBRATION = "power_calibration" +CONF_ENERGY_CALIBRATION = "energy_calibration" + +BL0940Number = bl0940_ns.class_("BL0940Number") + +CalibrationNumber = bl0940_ns.class_( + "CalibrationNumber", number.Number, cg.PollingComponent +) + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +CALIBRATION_SCHEMA = cv.All( + number.number_schema( + CalibrationNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + unit_of_measurement=UNIT_PERCENT, + ) + .extend( + { + cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES), + cv.Optional(CONF_MAX_VALUE, default=10): cv.All( + cv.float_, cv.Range(max=50) + ), + cv.Optional(CONF_MIN_VALUE, default=-10): cv.All( + cv.float_, cv.Range(min=-50) + ), + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA), + validate_min_max, +) + +# Configuration schema for BL0940 numbers +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0940Number), + cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940), + cv.Optional(CONF_CURRENT_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_VOLTAGE_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_POWER_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_ENERGY_CALIBRATION): CALIBRATION_SCHEMA, + } +) + + +async def to_code(config): + # Get the BL0940 component instance + bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) + + # Process all calibration types + for cal_type, setter_method in [ + (CONF_CURRENT_CALIBRATION, "set_current_calibration_number"), + (CONF_VOLTAGE_CALIBRATION, "set_voltage_calibration_number"), + (CONF_POWER_CALIBRATION, "set_power_calibration_number"), + (CONF_ENERGY_CALIBRATION, "set_energy_calibration_number"), + ]: + if conf := config.get(cal_type): + var = await number.new_number( + conf, + min_value=conf.get(CONF_MIN_VALUE), + max_value=conf.get(CONF_MAX_VALUE), + step=conf.get(CONF_STEP), + ) + await cg.register_component(var, conf) + + if restore_value := config.get(CONF_RESTORE_VALUE): + cg.add(var.set_restore_value(restore_value)) + cg.add(getattr(bl0940, setter_method)(var)) diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp new file mode 100644 index 0000000000..cdb26cd298 --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -0,0 +1,29 @@ +#include "calibration_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.number"; + +void CalibrationNumber::setup() { + float value = 0.0f; + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + value = 0.0f; + } + } + this->publish_state(value); +} + +void CalibrationNumber::control(float value) { + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); +} + +void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/calibration_number.h b/esphome/components/bl0940/number/calibration_number.h new file mode 100644 index 0000000000..3a19e36dc9 --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace bl0940 { + +class CalibrationNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + bool restore_value_{true}; + + ESPPreferenceObject pref_; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index f49e961f0a..d2e0ea435d 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL_TEMPERATURE, CONF_POWER, + CONF_REFERENCE_VOLTAGE, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -23,12 +24,133 @@ from esphome.const import ( UNIT_WATT, ) +from . import bl0940_ns + DEPENDENCIES = ["uart"] - -bl0940_ns = cg.esphome_ns.namespace("bl0940") BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) +CONF_LEGACY_MODE = "legacy_mode" + +CONF_READ_COMMAND = "read_command" +CONF_WRITE_COMMAND = "write_command" + +CONF_RESISTOR_SHUNT = "resistor_shunt" +CONF_RESISTOR_ONE = "resistor_one" +CONF_RESISTOR_TWO = "resistor_two" + +CONF_CURRENT_REFERENCE = "current_reference" +CONF_ENERGY_REFERENCE = "energy_reference" +CONF_POWER_REFERENCE = "power_reference" +CONF_VOLTAGE_REFERENCE = "voltage_reference" + +DEFAULT_BL0940_READ_COMMAND = 0x58 +DEFAULT_BL0940_WRITE_COMMAND = 0xA1 + +# Values according to BL0940 application note: +# https://www.belling.com.cn/media/file_object/bel_product/BL0940/guide/BL0940_APPNote_TSSOP14_V1.04_EN.pdf +DEFAULT_BL0940_VREF = 1.218 # Vref = 1.218 +DEFAULT_BL0940_RL = 1 # RL = 1 mΩ +DEFAULT_BL0940_R1 = 0.51 # R1 = 0.51 kΩ +DEFAULT_BL0940_R2 = 1950 # R2 = 5 x 390 kΩ -> 1950 kΩ + +# ---------------------------------------------------- +# values from initial implementation +DEFAULT_BL0940_LEGACY_READ_COMMAND = 0x50 +DEFAULT_BL0940_LEGACY_WRITE_COMMAND = 0xA0 + +DEFAULT_BL0940_LEGACY_UREF = 33000 +DEFAULT_BL0940_LEGACY_IREF = 275000 +DEFAULT_BL0940_LEGACY_PREF = 1430 +# Measured to 297J per click according to power consumption of 5 minutes +# Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 +DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 +# ---------------------------------------------------- + + +# methods to calculate voltage and current reference values +def calculate_voltage_reference(vref, r_one, r_two): + # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) + return 79931 / vref * (r_one * 1000) / (r_one + r_two) + + +def calculate_current_reference(vref, r_shunt): + # formula: 324004 * RL / Vref + return 324004 * r_shunt / vref + + +def calculate_power_reference(voltage_reference, current_reference): + # calculate power reference based on voltage and current reference + return voltage_reference * current_reference * 4046 / 324004 / 79931 + + +def calculate_energy_reference(power_reference): + # formula: power_reference * 3600000 / (1638.4 * 256) + return power_reference * 3600000 / (1638.4 * 256) + + +def validate_legacy_mode(config): + # Only allow schematic calibration options if legacy_mode is False + if config.get(CONF_LEGACY_MODE, True): + forbidden = [ + CONF_REFERENCE_VOLTAGE, + CONF_RESISTOR_SHUNT, + CONF_RESISTOR_ONE, + CONF_RESISTOR_TWO, + ] + for key in forbidden: + if key in config: + raise cv.Invalid( + f"Option '{key}' is only allowed when legacy_mode: false" + ) + return config + + +def set_command_defaults(config): + # Set defaults for read_command and write_command based on legacy_mode + legacy = config.get(CONF_LEGACY_MODE, True) + if legacy: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_LEGACY_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_LEGACY_WRITE_COMMAND) + else: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_WRITE_COMMAND) + return config + + +def set_reference_values(config): + # Set default reference values based on legacy_mode + if config.get(CONF_LEGACY_MODE, True): + config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) + config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) + config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + else: + vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) + r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) + r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) + r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) + + config.setdefault( + CONF_VOLTAGE_REFERENCE, calculate_voltage_reference(vref, r_one, r_two) + ) + config.setdefault( + CONF_CURRENT_REFERENCE, calculate_current_reference(vref, r_shunt) + ) + config.setdefault( + CONF_POWER_REFERENCE, + calculate_power_reference( + config.get(CONF_VOLTAGE_REFERENCE), config.get(CONF_CURRENT_REFERENCE) + ), + ) + config.setdefault( + CONF_ENERGY_REFERENCE, + calculate_energy_reference(config.get(CONF_POWER_REFERENCE)), + ) + + return config + + CONFIG_SCHEMA = ( cv.Schema( { @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_LEGACY_MODE, default=True): cv.boolean, + cv.Optional(CONF_READ_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_WRITE_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_REFERENCE_VOLTAGE): cv.float_, + cv.Optional(CONF_RESISTOR_SHUNT): cv.float_, + cv.Optional(CONF_RESISTOR_ONE): cv.float_, + cv.Optional(CONF_RESISTOR_TWO): cv.float_, + cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, + cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, + cv.Optional(CONF_POWER_REFERENCE): cv.float_, + cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, } ) .extend(cv.polling_component_schema("60s")) .extend(uart.UART_DEVICE_SCHEMA) + .add_extra(validate_legacy_mode) + .add_extra(set_command_defaults) + .add_extra(set_reference_values) ) @@ -99,3 +235,16 @@ async def to_code(config): if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): sens = await sensor.new_sensor(external_temperature_config) cg.add(var.set_external_temperature_sensor(sens)) + + # enable legacy mode + cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE))) + + # Set bl0940 commands after validator has determined which defaults to use if not set + cg.add(var.set_read_command(config.get(CONF_READ_COMMAND))) + cg.add(var.set_write_command(config.get(CONF_WRITE_COMMAND))) + + # Set reference values after validator has set the values either from defaults or calculated + cg.add(var.set_current_reference(config.get(CONF_CURRENT_REFERENCE))) + cg.add(var.set_voltage_reference(config.get(CONF_VOLTAGE_REFERENCE))) + cg.add(var.set_power_reference(config.get(CONF_POWER_REFERENCE))) + cg.add(var.set_energy_reference(config.get(CONF_ENERGY_REFERENCE))) diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 97a997d2b4..443f3b0ff0 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -4,8 +4,14 @@ uart: rx_pin: ${rx_pin} baud_rate: 9600 +button: + - platform: bl0940 + bl0940_id: test_id + name: Cal Reset + sensor: - platform: bl0940 + id: test_id voltage: name: BL0940 Voltage current: @@ -18,3 +24,18 @@ sensor: name: BL0940 Internal temperature external_temperature: name: BL0940 External temperature + +number: + - platform: bl0940 + id: bl0940_number_id + bl0940_id: test_id + current_calibration: + name: Cal Current + min_value: -5 + max_value: 5 + voltage_calibration: + name: Cal Voltage + step: 0.01 + power_calibration: + name: Cal Power + disabled_by_default: true