From 61b17106126f2cb51c9558ede5e3113ac2193778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Kub=C3=AD=C4=8Dek?= Date: Sun, 11 Aug 2024 00:07:09 +0200 Subject: [PATCH] HLW8032 Single-phase metering IC --- esphome/components/hlw8032/__init__.py | 1 + esphome/components/hlw8032/hlw8032.cpp | 198 +++++++++++++++++++++++++ esphome/components/hlw8032/hlw8032.h | 46 ++++++ esphome/components/hlw8032/sensor.py | 92 ++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 esphome/components/hlw8032/__init__.py create mode 100644 esphome/components/hlw8032/hlw8032.cpp create mode 100644 esphome/components/hlw8032/hlw8032.h create mode 100644 esphome/components/hlw8032/sensor.py diff --git a/esphome/components/hlw8032/__init__.py b/esphome/components/hlw8032/__init__.py new file mode 100644 index 0000000000..4908e10037 --- /dev/null +++ b/esphome/components/hlw8032/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@rici4kubicek"] diff --git a/esphome/components/hlw8032/hlw8032.cpp b/esphome/components/hlw8032/hlw8032.cpp new file mode 100644 index 0000000000..8c1b595aaf --- /dev/null +++ b/esphome/components/hlw8032/hlw8032.cpp @@ -0,0 +1,198 @@ +#include "hlw8032.h" +#include "esphome/core/log.h" +#include +#include +#include + +namespace esphome { +namespace hlw8032 { + +static const char *const TAG = "hlw8032"; + +void HLW8032Component::loop() { + + if (!this->available()) + return; + + uint8_t data = this->read(); + + if (((data == 0x55) || (data == 0xaa) || (data & 0xf0)) && !this->header_found_) { + this->header_found_ = true; + this->raw_data_[0] = data; + } else if (data == 0x5A && this->header_found_) { + this->raw_data_[1] = data; + this->raw_data_index_ = 2; + this->check_ = 0; + } else if (this->raw_data_index_ >= 2 && this->raw_data_index_ < 24) { + this->raw_data_[this->raw_data_index_] = data; + if (this->raw_data_index_ < 23) { + this->check_ += data; + } + this->raw_data_index_++; + if (this->raw_data_index_ == 24) { + if (this->check_ == this->raw_data_[23]) { + this->parse_data_(); + } else + ESP_LOGW(TAG, "Invalid checksum from HLW8032: 0x%02X != 0x%02X", this->check_, this->raw_data_[23]); + + this->raw_data_index_ = 0; + this->header_found_ = false; + memset(this->raw_data_, 0, 24); + } + } +} + +void HLW8032Component::parse_data_() { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +{ + std::stringstream ss; + ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); + for (unsigned char i : this->raw_data_) { + ss << ' ' << std::setw(2) << static_cast(i); + } + ESP_LOGD(TAG, "%s", ss.str().c_str()); +} +#endif + + // Parse header + uint8_t state_reg = this->raw_data_[0]; + + if (state_reg == 0xAA) { + ESP_LOGE(TAG, "HLW8032's function of error correction fails."); + return; + } + + // Parse data frame + uint32_t voltage_parameter = this->get_24_bit_uint_(2); + uint32_t voltage_reg = this->get_24_bit_uint_(5); + uint32_t current_parameter = this->get_24_bit_uint_(8); + uint32_t current_reg = this->get_24_bit_uint_(11); + uint32_t power_parameter = this->get_24_bit_uint_(14); + uint32_t power_reg = this->get_24_bit_uint_(17); + + uint8_t data_update_register = this->raw_data_[20]; + + bool have_power = data_update_register & (1 << 4); + bool have_current = data_update_register & (1 << 5); + bool have_voltage = data_update_register & (1 << 6); + + bool power_cycle_exceeds_range = false; + if ((state_reg & 0xF0) == 0xF0) { + if (state_reg & 0xF) { + ESP_LOGW(TAG, "HLW8032 reports: (0x%02X)", state_reg); + if (state_reg & (1 << 3)) { + ESP_LOGW(TAG, " Voltage REG overflows."); + have_voltage = false; + } + if (state_reg & (1 << 2)) { + ESP_LOGW(TAG, " Current REG overflows."); + have_current = false; + } + if (state_reg & (1 << 1)) { + ESP_LOGW(TAG, " Power REG overflows."); + have_power = false; + } + if (state_reg & (1 << 0)) { + ESP_LOGW(TAG, " Voltage Parameter REG, Current Parameter REG and Power Parameter REG is not usable."); + return; + } + } + power_cycle_exceeds_range = state_reg & (1 << 1); + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + ESP_LOGD(TAG, "HLW8032 Parsed data:"); + ESP_LOGD(TAG, " Voltage Parameter REG: 0x%06X, Voltage REG: 0x%06X", voltage_parameter, voltage_reg); + ESP_LOGD(TAG, " Current Parameter REG: 0x%06X, Current REG: 0x%06X", current_parameter, current_reg); + ESP_LOGD(TAG, " Power Parameter REG: 0x%06X, Power REG: 0x%06X", power_parameter, power_reg); + ESP_LOGD(TAG, " Data Update REG: 0x%02X", data_update_register); +#endif + + const float current_multiplier = 1 / (this->current_resistor_ * 1000); + + float voltage = 0.0f; + if (have_voltage) { + + voltage = float(voltage_parameter) * this->voltage_divider_ / float(voltage_reg); + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + } + + float power = 0.0f; + if (power_cycle_exceeds_range) { + // Datasheet: power cycle exceeding range means active power is 0 + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(0.0f); + } + } else if (have_power) { + power = (float(power_parameter) / float(power_reg)) * this->voltage_divider_ * current_multiplier; + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); + } + } + + float current = 0.0f; + if (have_current) { + current = float(current_parameter) * current_multiplier / float(current_reg); + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + } + + if (have_voltage && have_current) { + const float apparent_power = voltage * current; + if (this->apparent_power_sensor_ != nullptr) { + this->apparent_power_sensor_->publish_state(apparent_power); + } + if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { + float pf = NAN; + if (apparent_power > 0) { + pf = power / apparent_power; + if (pf < 0 || pf > 1) { + ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf); + pf = NAN; + } + } else if (apparent_power == 0 && power == 0) { + // No load, report ideal power factor + pf = 1.0f; + } + this->power_factor_sensor_->publish_state(pf); + } + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +{ + std::stringstream ss; + ss << "Parsed:"; + if (have_voltage) { + ss << " V=" << voltage << "V"; + } + if (have_current) { + ss << " I=" << current * 1000.0f << "mA"; + } + if (have_power) { + ss << " P=" << power << "W"; + } + ESP_LOGD(TAG, "%s", ss.str().c_str()); +} +#endif +} + +uint32_t HLW8032Component::get_24_bit_uint_(uint8_t start_index) { + return (uint32_t(this->raw_data_[start_index]) << 16) + (uint32_t(this->raw_data_[start_index + 1]) << 8) + + this->raw_data_[start_index + 2]; +} + +void HLW8032Component::dump_config() { + ESP_LOGCONFIG(TAG, "HLW8032:"); + ESP_LOGCONFIG(TAG, " Current resistor: %.1f mΩ", this->current_resistor_ * 1000.0f); + ESP_LOGCONFIG(TAG, " Voltage Divider: %.3f", this->voltage_divider_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_) + LOG_SENSOR(" ", "Current", this->current_sensor_) + LOG_SENSOR(" ", "Power", this->power_sensor_) + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_) + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_) +} +} // namespace hlw8032 +} // namespace esphome diff --git a/esphome/components/hlw8032/hlw8032.h b/esphome/components/hlw8032/hlw8032.h new file mode 100644 index 0000000000..914787cbbd --- /dev/null +++ b/esphome/components/hlw8032/hlw8032.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace hlw8032 { + +class HLW8032Component : public Component, public uart::UARTDevice { + public: + + void loop() override; + void dump_config() override; + + void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; } + void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; } + 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_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) { apparent_power_sensor_ = apparent_power_sensor; } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } + + protected: + void parse_data_(); + uint32_t get_24_bit_uint_(uint8_t start_index); + + bool header_found_{false}; + uint8_t check_{0}; + uint8_t raw_data_[24]{}; + uint8_t raw_data_index_{0}; + uint32_t last_transmission_{0}; + float current_resistor_{0.001}; + float voltage_divider_{1.720}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; +}; + +} // namespace hlw8032 +} // namespace esphome diff --git a/esphome/components/hlw8032/sensor.py b/esphome/components/hlw8032/sensor.py new file mode 100644 index 0000000000..72ee6592b1 --- /dev/null +++ b/esphome/components/hlw8032/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_CURRENT_RESISTOR, + CONF_ID, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_VOLTAGE, + CONF_VOLTAGE_DIVIDER, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +hlw8032_ns = cg.esphome_ns.namespace("hlw8032") +HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(HLW8032Component), + 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=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, + cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, + } +).extend(uart.UART_DEVICE_SCHEMA) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN" +) + + +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 voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) + cg.add(var.set_current_sensor(sens)) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) + cg.add(var.set_power_sensor(sens)) + if apparent_power_config := config.get(CONF_APPARENT_POWER): + sens = await sensor.new_sensor(apparent_power_config) + cg.add(var.set_apparent_power_sensor(sens)) + if power_factor_config := config.get(CONF_POWER_FACTOR): + sens = await sensor.new_sensor(power_factor_config) + cg.add(var.set_power_factor_sensor(sens)) + cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) + cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))