From 8ba207fc7f9ba08cb87eb31c9a020ce3541a4d2f Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sun, 11 Sep 2022 23:36:09 +0200 Subject: [PATCH] Add support for BL0942 voltage, current, energy and power Sensor (#3777) --- CODEOWNERS | 1 + esphome/components/bl0942/__init__.py | 1 + esphome/components/bl0942/bl0942.cpp | 121 ++++++++++++++++++++++++++ esphome/components/bl0942/bl0942.h | 68 +++++++++++++++ esphome/components/bl0942/sensor.py | 93 ++++++++++++++++++++ tests/test3.yaml | 12 +++ 6 files changed, 296 insertions(+) create mode 100644 esphome/components/bl0942/__init__.py create mode 100644 esphome/components/bl0942/bl0942.cpp create mode 100644 esphome/components/bl0942/bl0942.h create mode 100644 esphome/components/bl0942/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0441a6bfcd..e63528fabc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- +esphome/components/bl0942/* @dbuezas esphome/components/ble_client/* @buxtronix esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bl0942/__init__.py b/esphome/components/bl0942/__init__.py new file mode 100644 index 0000000000..8ef7857b7b --- /dev/null +++ b/esphome/components/bl0942/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@dbuezas"] diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp new file mode 100644 index 0000000000..e6d18a82a7 --- /dev/null +++ b/esphome/components/bl0942/bl0942.cpp @@ -0,0 +1,121 @@ +#include "bl0942.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0942 { + +static const char *const TAG = "bl0942"; + +static const uint8_t BL0942_READ_COMMAND = 0x58; +static const uint8_t BL0942_FULL_PACKET = 0xAA; +static const uint8_t BL0942_PACKET_HEADER = 0x55; + +static const uint8_t BL0942_WRITE_COMMAND = 0xA8; +static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10; +static const uint8_t BL0942_REG_MODE = 0x18; +static const uint8_t BL0942_REG_SOFT_RESET = 0x19; +static const uint8_t BL0942_REG_USR_WRPROT = 0x1A; +static const uint8_t BL0942_REG_TPS_CTRL = 0x1B; + +// TODO: Confirm insialisation works as intended +const uint8_t BL0942_INIT[5][6] = { + // Reset to default + {BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + // Enable User Operation Write + {BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS + {BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS + {BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + +void BL0942::loop() { + DataPacket buffer; + if (!this->available()) { + return; + } + if (read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (validate_checksum(&buffer)) { + received_package_(&buffer); + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + } +} + +bool BL0942::validate_checksum(DataPacket *data) { + uint8_t checksum = BL0942_READ_COMMAND; + // Whole package but checksum + 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, "BL0942 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + } + return checksum == data->checksum; +} + +void BL0942::update() { + this->flush(); + this->write_byte(BL0942_READ_COMMAND); + this->write_byte(BL0942_FULL_PACKET); +} + +void BL0942::setup() { + for (auto *i : BL0942_INIT) { + this->write_array(i, 6); + delay(1); + } + this->flush(); +} + +void BL0942::received_package_(DataPacket *data) { + // Bad header + if (data->frame_header != BL0942_PACKET_HEADER) { + ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); + return; + } + + float v_rms = (uint24_t) data->v_rms / voltage_reference_; + float i_rms = (uint24_t) data->i_rms / current_reference_; + float watt = (int24_t) data->watt / power_reference_; + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + float total_energy_consumption = cf_cnt / energy_reference_; + float frequency = 1000000.0f / data->frequency; + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(v_rms); + } + if (current_sensor_ != nullptr) { + current_sensor_->publish_state(i_rms); + } + if (power_sensor_ != nullptr) { + power_sensor_->publish_state(watt); + } + if (energy_sensor_ != nullptr) { + energy_sensor_->publish_state(total_energy_consumption); + } + if (frequency_sensor_ != nullptr) { + frequency_sensor_->publish_state(frequency); + } + + ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, frequency %f°Hz, status 0x%08X", v_rms, i_rms, watt, + cf_cnt, total_energy_consumption, frequency, data->status); +} + +void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity) + ESP_LOGCONFIG(TAG, "BL0942:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); + LOG_SENSOR("", "frequency", this->frequency_sensor_); +} + +} // namespace bl0942 +} // namespace esphome diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h new file mode 100644 index 0000000000..8149b7493b --- /dev/null +++ b/esphome/components/bl0942/bl0942.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/datatypes.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bl0942 { + +static const float BL0942_PREF = 596; // taken from tasmota +static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 +static const float BL0942_IREF = 251213.46469622; // 305978/1.218 +static const float BL0942_EREF = 3304.61127328; // Measured + +struct DataPacket { + uint8_t frame_header; + uint24_le_t i_rms; + uint24_le_t v_rms; + uint24_le_t i_fast_rms; + int24_le_t watt; + uint24_le_t cf_cnt; + uint16_le_t frequency; + uint8_t reserved1; + uint8_t status; + uint8_t reserved2; + uint8_t reserved3; + uint8_t checksum; +} __attribute__((packed)); + +class BL0942 : public PollingComponent, public uart::UARTDevice { + public: + 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; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + // NB This may be negative as the circuits is seemingly able to measure + // power in both directions + sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; + sensor::Sensor *frequency_sensor_; + + // Divide by this to turn into Watt + float power_reference_ = BL0942_PREF; + // Divide by this to turn into Volt + float voltage_reference_ = BL0942_UREF; + // Divide by this to turn into Ampere + float current_reference_ = BL0942_IREF; + // Divide by this to turn into kWh + float energy_reference_ = BL0942_EREF; + + static bool validate_checksum(DataPacket *data); + + void received_package_(DataPacket *data); +}; +} // namespace bl0942 +} // namespace esphome diff --git a/esphome/components/bl0942/sensor.py b/esphome/components/bl0942/sensor.py new file mode 100644 index 0000000000..f23375b309 --- /dev/null +++ b/esphome/components/bl0942/sensor.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CURRENT, + CONF_ENERGY, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + CONF_FREQUENCY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_FREQUENCY, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, + UNIT_HERTZ, +) + +DEPENDENCIES = ["uart"] + +bl0942_ns = cg.esphome_ns.namespace("bl0942") +BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0942), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + ), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) + if CONF_FREQUENCY in config: + conf = config[CONF_FREQUENCY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_frequency_sensor(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index de0fc0dfdb..1d4b4fb076 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -530,6 +530,18 @@ sensor: name: BL0940 Internal temperature external_temperature: name: BL0940 External temperature + - platform: bl0942 + uart_id: uart3 + voltage: + name: 'BL0942 Voltage' + current: + name: 'BL0942 Current' + power: + name: 'BL0942 Power' + energy: + name: 'BL0942 Energy' + frequency: + name: "BL0942 Frequency" - platform: pzem004t uart_id: uart3 voltage: