diff --git a/CODEOWNERS b/CODEOWNERS index b8f00aa368..30be9e74b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -81,6 +81,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core +esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index b0e43559f9..aaf9320b9e 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import ( CONF_ID, + CONF_REACTIVE_POWER, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, @@ -34,7 +35,6 @@ CONF_PHASE_A = "phase_a" CONF_PHASE_B = "phase_b" CONF_PHASE_C = "phase_c" -CONF_REACTIVE_POWER = "reactive_power" CONF_LINE_FREQUENCY = "line_frequency" CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_GAIN_PGA = "gain_pga" diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 08186ca46f..6b454cbaf0 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,7 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.cpp_helpers import gpio_pin_expression from esphome.components import uart -from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.const import CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_ADDRESS +from esphome import pins DEPENDENCIES = ["uart"] @@ -15,6 +17,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Modbus), + cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -29,6 +32,10 @@ async def to_code(config): await uart.register_uart_device(var, config) + if CONF_FLOW_CONTROL_PIN in config: + pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) + cg.add(var.set_flow_control_pin(pin)) + def modbus_device_schema(default_address): schema = { diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 74d0c40986..08eace9ef5 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -6,6 +6,11 @@ namespace modbus { static const char *TAG = "modbus"; +void Modbus::setup() { + if (this->flow_control_pin_ != nullptr) { + this->flow_control_pin_->setup(); + } +} void Modbus::loop() { const uint32_t now = millis(); if (now - this->last_modbus_byte_ > 50) { @@ -94,6 +99,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:"); + LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); this->check_uart_settings(9600, 2); } float Modbus::get_setup_priority() const { @@ -112,7 +118,14 @@ void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uin frame[6] = crc >> 0; frame[7] = crc >> 8; + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(true); + this->write_array(frame, 8); + this->flush(); + + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(false); } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index b75de147b1..91fc55b998 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -12,6 +12,8 @@ class Modbus : public uart::UARTDevice, public Component { public: Modbus() = default; + void setup() override; + void loop() override; void dump_config() override; @@ -22,7 +24,11 @@ class Modbus : public uart::UARTDevice, public Component { void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); + void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } + protected: + GPIOPin *flow_control_pin_{nullptr}; + bool parse_modbus_byte_(uint8_t byte); std::vector rx_buffer_; diff --git a/esphome/components/sdm_meter/__init__.py b/esphome/components/sdm_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp new file mode 100644 index 0000000000..de7c42421e --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -0,0 +1,106 @@ +#include "sdm_meter.h" +#include "sdm_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sdm_meter { + +static const char *TAG = "sdm_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers + +void SDMMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SDMMeter!"); + return; + } + + auto sdm_meter_get_float = [&](size_t i) -> float { + uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); + float f; + memcpy(&f, &temp, sizeof(f)); + return f; + }; + + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + + float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4)); + float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4)); + float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4)); + float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4)); + float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4)); + float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4)); + float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4)); + + ESP_LOGD( + TAG, + "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f VAR, PF=%.3f, " + "PA=%.3f °", + i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle); + if (phase.voltage_sensor_ != nullptr) + phase.voltage_sensor_->publish_state(voltage); + if (phase.current_sensor_ != nullptr) + phase.current_sensor_->publish_state(current); + if (phase.active_power_sensor_ != nullptr) + phase.active_power_sensor_->publish_state(active_power); + if (phase.apparent_power_sensor_ != nullptr) + phase.apparent_power_sensor_->publish_state(apparent_power); + if (phase.reactive_power_sensor_ != nullptr) + phase.reactive_power_sensor_->publish_state(reactive_power); + if (phase.power_factor_sensor_ != nullptr) + phase.power_factor_sensor_->publish_state(power_factor); + if (phase.phase_angle_sensor_ != nullptr) + phase.phase_angle_sensor_->publish_state(phase_angle); + } + + float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2); + float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2); + float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2); + float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2); + float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2); + + ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh", frequency, + import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy); + + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + 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->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); +} + +void SDMMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SDMMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SDM Meter:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + ESP_LOGCONFIG(TAG, " Phase %c", i + 'A'); + LOG_SENSOR(" ", "Voltage", phase.voltage_sensor_); + LOG_SENSOR(" ", "Current", phase.current_sensor_); + LOG_SENSOR(" ", "Active Power", phase.active_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", phase.apparent_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", phase.reactive_power_sensor_); + LOG_SENSOR(" ", "Power Factor", phase.power_factor_sensor_); + LOG_SENSOR(" ", "Phase Angle", phase.phase_angle_sensor_); + } + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); +} + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h new file mode 100644 index 0000000000..07ebe65bb7 --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace sdm_meter { + +class SDMMeter : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].current_sensor_ = current_sensor; + } + void set_active_power_sensor(uint8_t phase, sensor::Sensor *active_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].active_power_sensor_ = active_power_sensor; + } + void set_apparent_power_sensor(uint8_t phase, sensor::Sensor *apparent_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].apparent_power_sensor_ = apparent_power_sensor; + } + void set_reactive_power_sensor(uint8_t phase, sensor::Sensor *reactive_power_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].reactive_power_sensor_ = reactive_power_sensor; + } + void set_power_factor_sensor(uint8_t phase, sensor::Sensor *power_factor_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].power_factor_sensor_ = power_factor_sensor; + } + void set_phase_angle_sensor(uint8_t phase, sensor::Sensor *phase_angle_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].phase_angle_sensor_ = phase_angle_sensor; + } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } + void set_import_active_energy_sensor(sensor::Sensor *import_active_energy_sensor) { + this->import_active_energy_sensor_ = import_active_energy_sensor; + } + void set_export_active_energy_sensor(sensor::Sensor *export_active_energy_sensor) { + this->export_active_energy_sensor_ = export_active_energy_sensor; + } + void set_import_reactive_energy_sensor(sensor::Sensor *import_reactive_energy_sensor) { + this->import_reactive_energy_sensor_ = import_reactive_energy_sensor; + } + void set_export_reactive_energy_sensor(sensor::Sensor *export_reactive_energy_sensor) { + this->export_reactive_energy_sensor_ = export_reactive_energy_sensor; + } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + struct SDMPhase { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *phase_angle_sensor_{nullptr}; + } phases_[3]; + sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *import_active_energy_sensor_{nullptr}; + sensor::Sensor *export_active_energy_sensor_{nullptr}; + sensor::Sensor *import_reactive_energy_sensor_{nullptr}; + sensor::Sensor *export_reactive_energy_sensor_{nullptr}; +}; + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sdm_meter_registers.h b/esphome/components/sdm_meter/sdm_meter_registers.h new file mode 100644 index 0000000000..dd981d6f00 --- /dev/null +++ b/esphome/components/sdm_meter/sdm_meter_registers.h @@ -0,0 +1,114 @@ +#pragma once + +namespace esphome { +namespace sdm_meter { + +/* PHASE STATUS REGISTERS */ +static const uint16_t SDM_PHASE_1_VOLTAGE = 0x0000; +static const uint16_t SDM_PHASE_2_VOLTAGE = 0x0002; +static const uint16_t SDM_PHASE_3_VOLTAGE = 0x0004; +static const uint16_t SDM_PHASE_1_CURRENT = 0x0006; +static const uint16_t SDM_PHASE_2_CURRENT = 0x0008; +static const uint16_t SDM_PHASE_3_CURRENT = 0x000A; +static const uint16_t SDM_PHASE_1_ACTIVE_POWER = 0x000C; +static const uint16_t SDM_PHASE_2_ACTIVE_POWER = 0x000E; +static const uint16_t SDM_PHASE_3_ACTIVE_POWER = 0x0010; +static const uint16_t SDM_PHASE_1_APPARENT_POWER = 0x0012; +static const uint16_t SDM_PHASE_2_APPARENT_POWER = 0x0014; +static const uint16_t SDM_PHASE_3_APPARENT_POWER = 0x0016; +static const uint16_t SDM_PHASE_1_REACTIVE_POWER = 0x0018; +static const uint16_t SDM_PHASE_2_REACTIVE_POWER = 0x001A; +static const uint16_t SDM_PHASE_3_REACTIVE_POWER = 0x001C; +static const uint16_t SDM_PHASE_1_POWER_FACTOR = 0x001E; +static const uint16_t SDM_PHASE_2_POWER_FACTOR = 0x0020; +static const uint16_t SDM_PHASE_3_POWER_FACTOR = 0x0022; +static const uint16_t SDM_PHASE_1_ANGLE = 0x0024; +static const uint16_t SDM_PHASE_2_ANGLE = 0x0026; +static const uint16_t SDM_PHASE_3_ANGLE = 0x0028; + +static const uint16_t SDM_AVERAGE_L_TO_N_VOLTS = 0x002A; +static const uint16_t SDM_AVERAGE_LINE_CURRENT = 0x002E; +static const uint16_t SDM_SUM_LINE_CURRENT = 0x0030; +static const uint16_t SDM_TOTAL_SYSTEM_POWER = 0x0034; +static const uint16_t SDM_TOTAL_SYSTEM_APPARENT_POWER = 0x0038; +static const uint16_t SDM_TOTAL_SYSTEM_REACTIVE_POWER = 0x003C; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR = 0x003E; +static const uint16_t SDM_TOTAL_SYSTEM_PHASE_ANGLE = 0x0042; + +static const uint16_t SDM_FREQUENCY = 0x0046; + +static const uint16_t SDM_IMPORT_ACTIVE_ENERGY = 0x0048; +static const uint16_t SDM_EXPORT_ACTIVE_ENERGY = 0x004A; +static const uint16_t SDM_IMPORT_REACTIVE_ENERGY = 0x004C; +static const uint16_t SDM_EXPORT_REACTIVE_ENERGY = 0x004E; + +static const uint16_t SDM_VAH_SINCE_LAST_RESET = 0x0050; +static const uint16_t SDM_AH_SINCE_LAST_RESET = 0x0052; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_DEMAND = 0x0054; +static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND = 0x0056; +static const uint16_t SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND = 0x0058; +static const uint16_t SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND = 0x005A; +static const uint16_t SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND = 0x005C; +static const uint16_t SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND = 0x005E; +static const uint16_t SDM_TOTAL_SYSTEM_VA_DEMAND = 0x0064; +static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND = 0x0066; +static const uint16_t SDM_NEUTRAL_CURRENT_DEMAND = 0x0068; +static const uint16_t SDM_MAXIMUM_NEUTRAL_CURRENT = 0x006A; +static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS = 0x00C8; +static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS = 0x00CA; +static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS = 0x00CC; +static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS = 0x00CE; +static const uint16_t SDM_NEUTRAL_CURRENT = 0x00E0; + +static const uint16_t SDM_PHASE_1_LN_VOLTS_THD = 0x00EA; +static const uint16_t SDM_PHASE_2_LN_VOLTS_THD = 0x00EC; +static const uint16_t SDM_PHASE_3_LN_VOLTS_THD = 0x00EE; +static const uint16_t SDM_PHASE_1_CURRENT_THD = 0x00F0; +static const uint16_t SDM_PHASE_2_CURRENT_THD = 0x00F2; +static const uint16_t SDM_PHASE_3_CURRENT_THD = 0x00F4; + +static const uint16_t SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD = 0x00F8; +static const uint16_t SDM_AVERAGE_LINE_CURRENT_THD = 0x00FA; +static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR_INV = 0x00FE; +static const uint16_t SDM_PHASE_1_CURRENT_DEMAND = 0x0102; +static const uint16_t SDM_PHASE_2_CURRENT_DEMAND = 0x0104; +static const uint16_t SDM_PHASE_3_CURRENT_DEMAND = 0x0106; +static const uint16_t SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND = 0x0108; +static const uint16_t SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND = 0x010A; +static const uint16_t SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND = 0x010C; +static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS_THD = 0x014E; +static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS_THD = 0x0150; +static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS_THD = 0x0152; +static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD = 0x0154; + +static const uint16_t SDM_TOTAL_ACTIVE_ENERGY = 0x0156; +static const uint16_t SDM_TOTAL_REACTIVE_ENERGY = 0x0158; + +static const uint16_t SDM_L1_IMPORT_ACTIVE_ENERGY = 0x015A; +static const uint16_t SDM_L2_IMPORT_ACTIVE_ENERGY = 0x015C; +static const uint16_t SDM_L3_IMPORT_ACTIVE_ENERGY = 0x015E; +static const uint16_t SDM_L1_EXPORT_ACTIVE_ENERGY = 0x0160; +static const uint16_t SDM_L2_EXPORT_ACTIVE_ENERGY = 0x0162; +static const uint16_t SDM_L3_EXPORT_ACTIVE_ENERGY = 0x0164; +static const uint16_t SDM_L1_TOTAL_ACTIVE_ENERGY = 0x0166; +static const uint16_t SDM_L2_TOTAL_ACTIVE_ENERGY = 0x0168; +static const uint16_t SDM_L3_TOTAL_ACTIVE_ENERGY = 0x016a; +static const uint16_t SDM_L1_IMPORT_REACTIVE_ENERGY = 0x016C; +static const uint16_t SDM_L2_IMPORT_REACTIVE_ENERGY = 0x016E; +static const uint16_t SDM_L3_IMPORT_REACTIVE_ENERGY = 0x0170; +static const uint16_t SDM_L1_EXPORT_REACTIVE_ENERGY = 0x0172; +static const uint16_t SDM_L2_EXPORT_REACTIVE_ENERGY = 0x0174; +static const uint16_t SDM_L3_EXPORT_REACTIVE_ENERGY = 0x0176; +static const uint16_t SDM_L1_TOTAL_REACTIVE_ENERGY = 0x0178; +static const uint16_t SDM_L2_TOTAL_REACTIVE_ENERGY = 0x017A; +static const uint16_t SDM_L3_TOTAL_REACTIVE_ENERGY = 0x017C; + +static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY = 0x0180; +static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY = 0x0182; +static const uint16_t SDM_CURRENT_RESETTABLE_IMPORT_ENERGY = 0x0184; +static const uint16_t SDM_CURRENT_RESETTABLE_EXPORT_ENERGY = 0x0186; +static const uint16_t SDM_IMPORT_POWER = 0x0500; +static const uint16_t SDM_EXPORT_POWER = 0x0502; + +} // namespace sdm_meter +} // namespace esphome diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py new file mode 100644 index 0000000000..0f7a4c928b --- /dev/null +++ b/esphome/components/sdm_meter/sensor.py @@ -0,0 +1,131 @@ +from esphome.components.atm90e32.sensor import CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C +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_PHASE_ANGLE, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + ICON_FLASH, + UNIT_AMPERE, + UNIT_DEGREES, + UNIT_EMPTY, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_VOLT_AMPS_REACTIVE_HOURS, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@polyfaces", "@jesserockz"] + +sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter") +SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice) + +PHASE_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 2, DEVICE_CLASS_POWER + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, ICON_EMPTY, 2, DEVICE_CLASS_POWER + ), + CONF_POWER_FACTOR: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR + ), + CONF_PHASE_ANGLE: sensor.sensor_schema(UNIT_DEGREES, ICON_FLASH, 3), +} + +PHASE_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()} +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SDMMeter), + cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 3 + ), + cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + } + ) + .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) + + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_frequency_sensor(sens)) + + if CONF_IMPORT_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_IMPORT_ACTIVE_ENERGY]) + cg.add(var.set_import_active_energy_sensor(sens)) + + if CONF_EXPORT_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_EXPORT_ACTIVE_ENERGY]) + cg.add(var.set_export_active_energy_sensor(sens)) + + if CONF_IMPORT_REACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_IMPORT_REACTIVE_ENERGY]) + cg.add(var.set_import_reactive_energy_sensor(sens)) + + if CONF_EXPORT_REACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_EXPORT_REACTIVE_ENERGY]) + cg.add(var.set_export_reactive_energy_sensor(sens)) + + for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): + if phase not in config: + continue + + phase_config = config[phase] + for sensor_type in PHASE_SENSORS: + if sensor_type in phase_config: + sens = await sensor.new_sensor(phase_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor")(i, sens)) diff --git a/esphome/const.py b/esphome/const.py index d44e995fc8..2316cee7dc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -55,12 +55,14 @@ CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" CONF_ACTION_ID = "action_id" +CONF_ACTIVE_POWER = "active_power" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_AND = "and" CONF_AP = "ap" +CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" CONF_ARGS = "args" CONF_ASSUMED_STATE = "assumed_state" @@ -194,6 +196,8 @@ CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" +CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" +CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" CONF_EXTERNAL_COMPONENTS = "external_components" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" @@ -218,6 +222,7 @@ CONF_FILTERS = "filters" CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" +CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FORMALDEHYDE = "formaldehyde" @@ -261,6 +266,8 @@ CONF_IF = "if" CONF_IIR_FILTER = "iir_filter" CONF_ILLUMINANCE = "illuminance" CONF_IMPEDANCE = "impedance" +CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy" +CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" @@ -413,6 +420,7 @@ CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_PERIOD = "period" +CONF_PHASE_ANGLE = "phase_angle" CONF_PHASE_BALANCER = "phase_balancer" CONF_PIN = "pin" CONF_PIN_A = "pin_a" @@ -455,6 +463,7 @@ CONF_RATE = "rate" CONF_RAW = "raw" CONF_RC_CODE_1 = "rc_code_1" CONF_RC_CODE_2 = "rc_code_2" +CONF_REACTIVE_POWER = "reactive_power" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" @@ -715,6 +724,7 @@ UNIT_STEPS = "steps" UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" UNIT_VOLT_AMPS_REACTIVE = "VAR" +UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" diff --git a/tests/test3.yaml b/tests/test3.yaml index 0a01405516..dcffdb8504 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -469,6 +469,62 @@ sensor: name: "Fingerprint Last Finger ID" last_confidence: name: "Fingerprint Last Confidence" + - platform: sdm_meter + phase_a: + current: + name: 'Phase A Current' + voltage: + name: 'Phase A Voltage' + active_power: + name: 'Phase A Power' + power_factor: + name: 'Phase A Power Factor' + apparent_power: + name: 'Phase A Apparent Power' + reactive_power: + name: 'Phase A Reactive Power' + phase_angle: + name: 'Phase A Phase Angle' + phase_b: + current: + name: 'Phase B Current' + voltage: + name: 'Phase B Voltage' + active_power: + name: 'Phase B Power' + power_factor: + name: 'Phase B Power Factor' + apparent_power: + name: 'Phase B Apparent Power' + reactive_power: + name: 'Phase B Reactive Power' + phase_angle: + name: 'Phase B Phase Angle' + phase_c: + current: + name: 'Phase C Current' + voltage: + name: 'Phase C Voltage' + active_power: + name: 'Phase C Power' + power_factor: + name: 'Phase C Power Factor' + apparent_power: + name: 'Phase C Apparent Power' + reactive_power: + name: 'Phase C Reactive Power' + phase_angle: + name: 'Phase C Phase Angle' + frequency: + name: 'Frequency' + import_active_energy: + name: 'Import Active Energy' + export_active_energy: + name: 'Export Active Energy' + import_reactive_energy: + name: 'Import Reactive Energy' + export_reactive_energy: + name: 'Export Reactive Energy' time: - platform: homeassistant