diff --git a/CODEOWNERS b/CODEOWNERS index 1decc5c33..844bea763 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -97,6 +97,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces +esphome/components/selec_meter/* @sourabhjaiswal esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny diff --git a/esphome/components/selec_meter/__init__.py b/esphome/components/selec_meter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp new file mode 100644 index 000000000..8bcf91f3c --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -0,0 +1,108 @@ +#include "selec_meter.h" +#include "selec_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace selec_meter { + +static const char *const TAG = "selec_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers + +void SelecMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SelecMeter!"); + return; + } + + auto selec_meter_get_float = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]); + + float f; + memcpy(&f, &temp, sizeof(f)); + return (f * unit); + }; + + float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT); + float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT); + float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT); + float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT); + float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT); + float maximum_demand_active_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_reactive_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_apparent_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + + if (this->total_active_energy_sensor_ != nullptr) + this->total_active_energy_sensor_->publish_state(total_active_energy); + if (this->import_active_energy_sensor_ != nullptr) + this->import_active_energy_sensor_->publish_state(import_active_energy); + if (this->export_active_energy_sensor_ != nullptr) + this->export_active_energy_sensor_->publish_state(export_active_energy); + if (this->total_reactive_energy_sensor_ != nullptr) + this->total_reactive_energy_sensor_->publish_state(total_reactive_energy); + if (this->import_reactive_energy_sensor_ != nullptr) + this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); + if (this->export_reactive_energy_sensor_ != nullptr) + this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); + if (this->apparent_energy_sensor_ != nullptr) + this->apparent_energy_sensor_->publish_state(apparent_energy); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->apparent_power_sensor_ != nullptr) + this->apparent_power_sensor_->publish_state(apparent_power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->maximum_demand_active_power_sensor_ != nullptr) + this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power); + if (this->maximum_demand_reactive_power_sensor_ != nullptr) + this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power); + if (this->maximum_demand_apparent_power_sensor_ != nullptr) + this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); +} + +void SelecMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SelecMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SELEC Meter:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); + LOG_SENSOR(" ", "Apparent Energy", this->apparent_energy_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Maximum Demand Active Power", this->maximum_demand_active_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Reactive Power", this->maximum_demand_reactive_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_); +} + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h new file mode 100644 index 000000000..0477cd2a6 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace selec_meter { + +#define SELEC_METER_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; } + +class SelecMeter : public PollingComponent, public modbus::ModbusDevice { + public: + SELEC_METER_SENSOR(total_active_energy) + SELEC_METER_SENSOR(import_active_energy) + SELEC_METER_SENSOR(export_active_energy) + SELEC_METER_SENSOR(total_reactive_energy) + SELEC_METER_SENSOR(import_reactive_energy) + SELEC_METER_SENSOR(export_reactive_energy) + SELEC_METER_SENSOR(apparent_energy) + SELEC_METER_SENSOR(active_power) + SELEC_METER_SENSOR(reactive_power) + SELEC_METER_SENSOR(apparent_power) + SELEC_METER_SENSOR(voltage) + SELEC_METER_SENSOR(current) + SELEC_METER_SENSOR(power_factor) + SELEC_METER_SENSOR(frequency) + SELEC_METER_SENSOR(maximum_demand_active_power) + SELEC_METER_SENSOR(maximum_demand_reactive_power) + SELEC_METER_SENSOR(maximum_demand_apparent_power) + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; +}; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter_registers.h b/esphome/components/selec_meter/selec_meter_registers.h new file mode 100644 index 000000000..dfaf65ff0 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter_registers.h @@ -0,0 +1,32 @@ +#pragma once + +namespace esphome { +namespace selec_meter { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; +static const float MULTIPLY_THOUSAND_UNIT = 1000; + +/* PHASE STATUS REGISTERS */ +static const uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000; +static const uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002; +static const uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004; +static const uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006; +static const uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008; +static const uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A; +static const uint16_t SELEC_APPARENT_ENERGY = 0x000C; +static const uint16_t SELEC_ACTIVE_POWER = 0x000E; +static const uint16_t SELEC_REACTIVE_POWER = 0x0010; +static const uint16_t SELEC_APPARENT_POWER = 0x0012; +static const uint16_t SELEC_VOLTAGE = 0x0014; +static const uint16_t SELEC_CURRENT = 0x0016; +static const uint16_t SELEC_POWER_FACTOR = 0x0018; +static const uint16_t SELEC_FREQUENCY = 0x001A; +static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C; +static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E; +static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py new file mode 100644 index 000000000..7eb526aec --- /dev/null +++ b/esphome/components/selec_meter/sensor.py @@ -0,0 +1,169 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_EXPORT_ACTIVE_ENERGY, + CONF_EXPORT_REACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_IMPORT_ACTIVE_ENERGY, + CONF_IMPORT_REACTIVE_ENERGY, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_EMPTY, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +CONF_TOTAL_ACTIVE_ENERGY = "total_active_energy" +CONF_TOTAL_REACTIVE_ENERGY = "total_reactive_energy" +CONF_APPARENT_ENERGY = "apparent_energy" +CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power" +CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power" +CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power" + +UNIT_KILOWATT_HOURS = "kWh" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" + +selec_meter_ns = cg.esphome_ns.namespace("selec_meter") +SelecMeter = selec_meter_ns.class_( + "SelecMeter", cg.PollingComponent, modbus.ModbusDevice +) + +SENSORS = { + CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_APPARENT_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + ), + CONF_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + CONF_POWER_FACTOR: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + ), + CONF_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), +} + +CONFIG_SCHEMA = ( + cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)}) + .extend( + {cv.Optional(sensor_name): schema for sensor_name, schema in SENSORS.items()} + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + for name in SENSORS: + if name in config: + sens = await sensor.new_sensor(config[name]) + cg.add(getattr(var, f"set_{name}_sensor")(sens)) diff --git a/tests/test5.yaml b/tests/test5.yaml index 35225402a..5d1c99d77 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -18,6 +18,13 @@ ota: logger: +uart: + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + +modbus: + binary_sensor: - platform: gpio pin: GPIO0 @@ -55,3 +62,40 @@ number: max_value: 100 min_value: 0 step: 5 + +sensor: + - platform: selec_meter + total_active_energy: + name: "SelecEM2M Total Active Energy" + import_active_energy: + name: "SelecEM2M Import Active Energy" + export_active_energy: + name: "SelecEM2M Export Active Energy" + total_reactive_energy: + name: "SelecEM2M Total Reactive Energy" + import_reactive_energy: + name: "SelecEM2M Import Reactive Energy" + export_reactive_energy: + name: "SelecEM2M Export Reactive Energy" + apparent_energy: + name: "SelecEM2M Apparent Energy" + active_power: + name: "SelecEM2M Active Power" + reactive_power: + name: "SelecEM2M Reactive Power" + apparent_power: + name: "SelecEM2M Apparent Power" + voltage: + name: "SelecEM2M Voltage" + current: + name: "SelecEM2M Current" + power_factor: + name: "SelecEM2M Power Factor" + frequency: + name: "SelecEM2M Frequency" + maximum_demand_active_power: + name: "SelecEM2M Maximum Demand Active Power" + maximum_demand_reactive_power: + name: "SelecEM2M Maximum Demand Reactive Power" + maximum_demand_apparent_power: + name: "SelecEM2M Maximum Demand Apparent Power"