mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	[bl0940] extend configuration options of bl0940 device (#8158)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
		| @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core | |||||||
| esphome/components/bk72xx/* @kuba2k2 | esphome/components/bk72xx/* @kuba2k2 | ||||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||||
| esphome/components/bl0939/* @ziceva | esphome/components/bl0939/* @ziceva | ||||||
| esphome/components/bl0940/* @tobias- | esphome/components/bl0940/* @dan-s-github @tobias- | ||||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | esphome/components/bl0942/* @dbuezas @dwmw2 | ||||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | esphome/components/ble_client/* @buxtronix @clydebarrow | ||||||
| esphome/components/bluetooth_proxy/* @bdraco @jesserockz | esphome/components/bluetooth_proxy/* @bdraco @jesserockz | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -7,28 +7,26 @@ namespace bl0940 { | |||||||
|  |  | ||||||
| static const char *const TAG = "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_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_I_FAST_RMS_CTRL = 0x10; | ||||||
| static const uint8_t BL0940_REG_MODE = 0x18; | static const uint8_t BL0940_REG_MODE = 0x18; | ||||||
| static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | ||||||
| static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | ||||||
| static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; | 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 |     // 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 |     // 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 |     // 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 |     // 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 |     // 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() { | void BL0940::loop() { | ||||||
|   DataPacket buffer; |   DataPacket buffer; | ||||||
| @@ -36,8 +34,8 @@ void BL0940::loop() { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { |   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||||
|     if (validate_checksum(&buffer)) { |     if (this->validate_checksum_(&buffer)) { | ||||||
|       received_package_(&buffer); |       this->received_package_(&buffer); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); |     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||||
| @@ -46,35 +44,151 @@ void BL0940::loop() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool BL0940::validate_checksum(const DataPacket *data) { | bool BL0940::validate_checksum_(DataPacket *data) { | ||||||
|   uint8_t checksum = BL0940_READ_COMMAND; |   uint8_t checksum = this->read_command_; | ||||||
|   // Whole package but checksum |   // Whole package but checksum | ||||||
|   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { |   uint8_t *raw = (uint8_t *) data; | ||||||
|     checksum += data->raw[i]; |   for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { | ||||||
|  |     checksum += raw[i]; | ||||||
|   } |   } | ||||||
|   checksum ^= 0xFF; |   checksum ^= 0xFF; | ||||||
|   if (checksum != data->checksum) { |   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; |   return checksum == data->checksum; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::update() { | void BL0940::update() { | ||||||
|   this->flush(); |   this->flush(); | ||||||
|   this->write_byte(BL0940_READ_COMMAND); |   this->write_byte(this->read_command_); | ||||||
|   this->write_byte(BL0940_FULL_PACKET); |   this->write_byte(BL0940_FULL_PACKET); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::setup() { | 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) { |   for (auto *i : BL0940_INIT) { | ||||||
|     this->write_array(i, 6); |     this->write_byte(this->write_command_), this->write_array(i, 5); | ||||||
|     delay(1); |     delay(1); | ||||||
|   } |   } | ||||||
|   this->flush(); |   this->flush(); | ||||||
| } | } | ||||||
|  |  | ||||||
| float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | float BL0940::calculate_power_reference_() { | ||||||
|   auto tb = (float) (temperature.h << 8 | temperature.l); |   // 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; |   float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; | ||||||
|   if (sensor != nullptr) { |   if (sensor != nullptr) { | ||||||
|     if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { |     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; |   return converted_temp; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::received_package_(const DataPacket *data) const { | void BL0940::received_package_(DataPacket *data) { | ||||||
|   // Bad header |   // Bad header | ||||||
|   if (data->frame_header != BL0940_PACKET_HEADER) { |   if (data->frame_header != BL0940_PACKET_HEADER) { | ||||||
|     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); |     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; |   // cf_cnt is only 24 bits, so track overflows | ||||||
|   float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; |   uint32_t cf_cnt = (uint24_t) data->cf_cnt; | ||||||
|   float watt = (float) to_int32_t(data->watt) / power_reference_; |   cf_cnt |= this->prev_cf_cnt_ & 0xff000000; | ||||||
|   uint32_t cf_cnt = to_uint32_t(data->cf_cnt); |   if (cf_cnt < this->prev_cf_cnt_) { | ||||||
|   float total_energy_consumption = (float) cf_cnt / energy_reference_; |     cf_cnt += 0x1000000; | ||||||
|  |   } | ||||||
|  |   this->prev_cf_cnt_ = cf_cnt; | ||||||
|  |  | ||||||
|   float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); |   float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; | ||||||
|   float tps2 = update_temp_(external_temperature_sensor_, data->tps2); |   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) { |   float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); | ||||||
|     voltage_sensor_->publish_state(v_rms); |   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) { |   if (this->current_sensor_ != nullptr) { | ||||||
|     current_sensor_->publish_state(i_rms); |     this->current_sensor_->publish_state(i_rms); | ||||||
|   } |   } | ||||||
|   if (power_sensor_ != nullptr) { |   if (this->power_sensor_ != nullptr) { | ||||||
|     power_sensor_->publish_state(watt); |     this->power_sensor_->publish_state(watt); | ||||||
|   } |   } | ||||||
|   if (energy_sensor_ != nullptr) { |   if (this->energy_sensor_ != nullptr) { | ||||||
|     energy_sensor_->publish_state(total_energy_consumption); |     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, |   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) | 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("", "Voltage", this->voltage_sensor_); | ||||||
|   LOG_SENSOR("", "Current", this->current_sensor_); |   LOG_SENSOR("", "Current", this->current_sensor_); | ||||||
|   LOG_SENSOR("", "Power", this->power_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_); |   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 bl0940 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,66 +1,48 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #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/sensor/sensor.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace bl0940 { | 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) | // Caveat: All these values are big endian (low - middle - high) | ||||||
|  | struct DataPacket { | ||||||
| union DataPacket {  // NOLINT(altera-struct-pack-align) |   uint8_t frame_header;    // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) | ||||||
|   uint8_t raw[35]; |   uint24_le_t i_fast_rms;  // Fast RMS current | ||||||
|   struct { |   uint24_le_t i_rms;       // RMS current | ||||||
|     uint8_t frame_header;  // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. |   uint24_t RESERVED0;      // Reserved | ||||||
|     ube24_t i_fast_rms;    // 0x00 |   uint24_le_t v_rms;       // RMS voltage | ||||||
|     ube24_t i_rms;         // 0x04 |   uint24_t RESERVED1;      // Reserved | ||||||
|     ube24_t RESERVED0;     // reserved |   int24_le_t watt;         // Active power (can be negative for bidirectional measurement) | ||||||
|     ube24_t v_rms;         // 0x06 |   uint24_t RESERVED2;      // Reserved | ||||||
|     ube24_t RESERVED1;     // reserved |   uint24_le_t cf_cnt;      // Energy pulse count | ||||||
|     sbe24_t watt;          // 0x08 |   uint24_t RESERVED3;      // Reserved | ||||||
|     ube24_t RESERVED2;     // reserved |   uint16_le_t tps1;        // Internal temperature sensor 1 | ||||||
|     ube24_t cf_cnt;        // 0x0A |   uint8_t RESERVED4;       // Reserved (should be 0x00) | ||||||
|     ube24_t RESERVED3;     // reserved |   uint16_le_t tps2;        // Internal temperature sensor 2 | ||||||
|     ube16_t tps1;          // 0x0c |   uint8_t RESERVED5;       // Reserved (should be 0x00) | ||||||
|     uint8_t RESERVED4;     // value of 0x00 |   uint8_t checksum;        // Packet checksum | ||||||
|     ube16_t tps2;          // 0x0c |  | ||||||
|     uint8_t RESERVED5;     // value of 0x00 |  | ||||||
|     uint8_t checksum;      // checksum |  | ||||||
|   }; |  | ||||||
| } __attribute__((packed)); | } __attribute__((packed)); | ||||||
|  |  | ||||||
| class BL0940 : public PollingComponent, public uart::UARTDevice { | class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||||
|  public: |  public: | ||||||
|  |   // Sensor setters | ||||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } |   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_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } | ||||||
|   void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_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; } |   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) { |   void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { | ||||||
|     internal_temperature_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; |     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 update() override; | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   sensor::Sensor *voltage_sensor_{nullptr}; |   // --- Sensor pointers --- | ||||||
|   sensor::Sensor *current_sensor_{nullptr}; |   sensor::Sensor *voltage_sensor_{nullptr};               // Voltage sensor | ||||||
|   // NB This may be negative as the circuits is seemingly able to measure |   sensor::Sensor *current_sensor_{nullptr};               // Current sensor | ||||||
|   // power in both directions |   sensor::Sensor *power_sensor_{nullptr};                 // Power sensor (can be negative for bidirectional) | ||||||
|   sensor::Sensor *power_sensor_{nullptr}; |   sensor::Sensor *energy_sensor_{nullptr};                // Energy sensor | ||||||
|   sensor::Sensor *energy_sensor_{nullptr}; |   sensor::Sensor *internal_temperature_sensor_{nullptr};  // Internal temperature sensor | ||||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr}; |   sensor::Sensor *external_temperature_sensor_{nullptr};  // External temperature sensor | ||||||
|   sensor::Sensor *external_temperature_sensor_{nullptr}; |  | ||||||
|  |  | ||||||
|   // Max difference between two measurements of the temperature. Used to avoid noise. | #ifdef USE_NUMBER | ||||||
|   float max_temperature_diff_{0}; |   // --- Calibration number entities (for dynamic calibration via HA UI) --- | ||||||
|   // Divide by this to turn into Watt |   number::Number *voltage_calibration_number_{nullptr}; | ||||||
|   float power_reference_ = BL0940_PREF; |   number::Number *current_calibration_number_{nullptr}; | ||||||
|   // Divide by this to turn into Volt |   number::Number *power_calibration_number_{nullptr}; | ||||||
|   float voltage_reference_ = BL0940_UREF; |   number::Number *energy_calibration_number_{nullptr}; | ||||||
|   // Divide by this to turn into Ampere | #endif | ||||||
|   float current_reference_ = BL0940_IREF; |  | ||||||
|   // Divide by this to turn into kWh |  | ||||||
|   float energy_reference_ = BL0940_EREF; |  | ||||||
|  |  | ||||||
|   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 bl0940 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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]) | ||||||
| @@ -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 | ||||||
							
								
								
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<BL0940> { | ||||||
|  |  public: | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void press_action() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bl0940 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||||
							
								
								
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<float>(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 | ||||||
							
								
								
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INTERNAL_TEMPERATURE, |     CONF_INTERNAL_TEMPERATURE, | ||||||
|     CONF_POWER, |     CONF_POWER, | ||||||
|  |     CONF_REFERENCE_VOLTAGE, | ||||||
|     CONF_VOLTAGE, |     CONF_VOLTAGE, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
| @@ -23,12 +24,133 @@ from esphome.const import ( | |||||||
|     UNIT_WATT, |     UNIT_WATT, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | from . import bl0940_ns | ||||||
|  |  | ||||||
| DEPENDENCIES = ["uart"] | DEPENDENCIES = ["uart"] | ||||||
|  |  | ||||||
|  |  | ||||||
| bl0940_ns = cg.esphome_ns.namespace("bl0940") |  | ||||||
| BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) | 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 = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( | |||||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 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(cv.polling_component_schema("60s")) | ||||||
|     .extend(uart.UART_DEVICE_SCHEMA) |     .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): |     if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): | ||||||
|         sens = await sensor.new_sensor(external_temperature_config) |         sens = await sensor.new_sensor(external_temperature_config) | ||||||
|         cg.add(var.set_external_temperature_sensor(sens)) |         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))) | ||||||
|   | |||||||
| @@ -4,8 +4,14 @@ uart: | |||||||
|     rx_pin: ${rx_pin} |     rx_pin: ${rx_pin} | ||||||
|     baud_rate: 9600 |     baud_rate: 9600 | ||||||
|  |  | ||||||
|  | button: | ||||||
|  |   - platform: bl0940 | ||||||
|  |     bl0940_id: test_id | ||||||
|  |     name: Cal Reset | ||||||
|  |  | ||||||
| sensor: | sensor: | ||||||
|   - platform: bl0940 |   - platform: bl0940 | ||||||
|  |     id: test_id | ||||||
|     voltage: |     voltage: | ||||||
|       name: BL0940 Voltage |       name: BL0940 Voltage | ||||||
|     current: |     current: | ||||||
| @@ -18,3 +24,18 @@ sensor: | |||||||
|       name: BL0940 Internal temperature |       name: BL0940 Internal temperature | ||||||
|     external_temperature: |     external_temperature: | ||||||
|       name: BL0940 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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user