diff --git a/components/soyosource_virtual_meter/__init__.py b/components/soyosource_virtual_meter/__init__.py index 03f43d1..b0fd017 100644 --- a/components/soyosource_virtual_meter/__init__.py +++ b/components/soyosource_virtual_meter/__init__.py @@ -19,6 +19,7 @@ CONF_POWER_DEMAND_DIVIDER = "power_demand_divider" CONF_OPERATION_STATUS_ID = "operation_status_id" CONF_ZERO_OUTPUT_ON_MIN_POWER_DEMAND = "zero_output_on_min_power_demand" +CONF_MAX_POWER_SENSOR_LATENCY_MS = "max_power_sensor_latency_ms" DEFAULT_MIN_BUFFER = -200 DEFAULT_MAX_BUFFER = 200 @@ -31,6 +32,10 @@ DEFAULT_MIN_POWER_DEMAND_DIVIDER = 1 DEFAULT_MAX_POWER_DEMAND_DIVIDER = 6 +DEFAULT_MAX_POWER_SENSOR_LATENCY_MS = 5000 +DEFAULT_MIN_MAX_POWER_SENSOR_LATENCY_MS = 0 +DEFAULT_MAX_MAX_POWER_SENSOR_LATENCY_MS = 30000 + soyosource_virtual_meter_ns = cg.esphome_ns.namespace("soyosource_virtual_meter") SoyosourceVirtualMeter = soyosource_virtual_meter_ns.class_( "SoyosourceVirtualMeter", @@ -85,6 +90,13 @@ def validate_min_max(config): CONF_MAX_POWER_DEMAND, default=DEFAULT_MAX_POWER_DEMAND ): cv.int_range(min=1, max=5400), cv.Optional(CONF_ZERO_OUTPUT_ON_MIN_POWER_DEMAND, default=True): cv.boolean, + cv.Optional( + CONF_MAX_POWER_SENSOR_LATENCY_MS, + default=DEFAULT_MAX_POWER_SENSOR_LATENCY_MS, + ): cv.int_range( + min=DEFAULT_MIN_MAX_POWER_SENSOR_LATENCY_MS, + max=DEFAULT_MAX_MAX_POWER_SENSOR_LATENCY_MS, + ), } ) .extend(soyosource_modbus.soyosource_modbus_device_schema(0x24)) @@ -116,6 +128,11 @@ async def to_code(config): ) ) cg.add(var.set_power_demand_calculation(config[CONF_POWER_DEMAND_CALCULATION])) + cg.add( + var.set_power_demand_compensation_timeout_ms( + config[CONF_MAX_POWER_SENSOR_LATENCY_MS] + ) + ) if CONF_OPERATION_STATUS_ID in config: operation_status_sensor = await cg.get_variable( diff --git a/components/soyosource_virtual_meter/number/__init__.py b/components/soyosource_virtual_meter/number/__init__.py index 3050371..ca13c53 100644 --- a/components/soyosource_virtual_meter/number/__init__.py +++ b/components/soyosource_virtual_meter/number/__init__.py @@ -10,12 +10,14 @@ CONF_STEP, UNIT_EMPTY, UNIT_WATT, + UNIT_MILLISECOND, ) from .. import ( CONF_BUFFER, CONF_POWER_DEMAND_DIVIDER, CONF_SOYOSOURCE_VIRTUAL_METER_ID, + CONF_MAX_POWER_SENSOR_LATENCY_MS, DEFAULT_BUFFER, DEFAULT_MAX_BUFFER, DEFAULT_MAX_POWER_DEMAND, @@ -24,6 +26,9 @@ DEFAULT_MIN_POWER_DEMAND, DEFAULT_MIN_POWER_DEMAND_DIVIDER, DEFAULT_POWER_DEMAND_DIVIDER, + DEFAULT_MAX_POWER_SENSOR_LATENCY_MS, + DEFAULT_MIN_MAX_POWER_SENSOR_LATENCY_MS, + DEFAULT_MAX_MAX_POWER_SENSOR_LATENCY_MS, SoyosourceVirtualMeter, soyosource_virtual_meter_ns, ) @@ -40,12 +45,14 @@ ICON_MANUAL_POWER_DEMAND = "mdi:home-lightning-bolt-outline" ICON_MAX_POWER_DEMAND = "mdi:transmission-tower-import" ICON_POWER_DEMAND_DIVIDER = "mdi:chart-arc" +ICON_POWER_SENSOR_LATENCY = "mdi:timer-sync-outline" NUMBERS = { CONF_MANUAL_POWER_DEMAND: 0x00, CONF_MAX_POWER_DEMAND: 0x01, CONF_BUFFER: 0x02, CONF_POWER_DEMAND_DIVIDER: 0x03, + CONF_MAX_POWER_SENSOR_LATENCY_MS: 0x04, } SoyosourceNumber = soyosource_virtual_meter_ns.class_( @@ -164,6 +171,31 @@ def validate(config): validate_min_max, validate, ), + cv.Optional(CONF_MAX_POWER_SENSOR_LATENCY_MS): cv.All( + number.number_schema( + SoyosourceNumber, + icon=ICON_POWER_SENSOR_LATENCY, + unit_of_measurement=UNIT_MILLISECOND, + ) + .extend( + { + cv.Optional( + CONF_MIN_VALUE, default=DEFAULT_MIN_MAX_POWER_SENSOR_LATENCY_MS + ): cv.float_, + cv.Optional( + CONF_MAX_VALUE, default=DEFAULT_MAX_MAX_POWER_SENSOR_LATENCY_MS + ): cv.float_, + cv.Optional(CONF_STEP, default=DEFAULT_STEP): cv.float_, + cv.Optional( + CONF_INITIAL_VALUE, default=DEFAULT_MAX_POWER_SENSOR_LATENCY_MS + ): cv.float_, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA), + validate_min_max, + validate, + ), } ) diff --git a/components/soyosource_virtual_meter/number/soyosource_number.cpp b/components/soyosource_virtual_meter/number/soyosource_number.cpp index 8128e78..8f17a86 100644 --- a/components/soyosource_virtual_meter/number/soyosource_number.cpp +++ b/components/soyosource_virtual_meter/number/soyosource_number.cpp @@ -42,6 +42,9 @@ void SoyosourceNumber::apply_and_publish_(float value) { case 0x03: this->parent_->set_power_demand_divider((uint8_t) value); break; + case 0x04: + this->parent_->set_power_demand_compensation_timeout_ms((uint16_t) value); + break; } this->publish_state(value); diff --git a/components/soyosource_virtual_meter/soyosource_virtual_meter.cpp b/components/soyosource_virtual_meter/soyosource_virtual_meter.cpp index 621ead2..b635c69 100644 --- a/components/soyosource_virtual_meter/soyosource_virtual_meter.cpp +++ b/components/soyosource_virtual_meter/soyosource_virtual_meter.cpp @@ -20,6 +20,11 @@ void SoyosourceVirtualMeter::setup() { this->last_power_demand_); this->last_power_demand_received_ = millis(); + + // If the update_interval is set to never, call the update() method on every power sensor update + if (this->get_update_interval() == SCHEDULER_DONT_RUN) { + this->update(); + } }); } @@ -106,15 +111,41 @@ int16_t SoyosourceVirtualMeter::calculate_power_demand_negative_measurements_(in // -500 -490 10 500 0 0 // -700 -690 10 500 -200 0 int16_t importing_now = consumption - this->buffer_; - int16_t power_demand = importing_now + last_power_demand; + + reset_power_demand_compensation_(importing_now); + + int16_t power_demand = importing_now + last_power_demand - this->power_demand_compensation_; if (power_demand >= this->max_power_demand_) { - return this->max_power_demand_; + power_demand = max_power_demand_; } if (power_demand < this->min_power_demand_) { - return (this->zero_output_on_min_power_demand_) ? 0 : this->min_power_demand_; + power_demand = (this->zero_output_on_min_power_demand_) ? 0 : this->min_power_demand_; + } + + ESP_LOGD(TAG, "'%s': updated power_demand_compensation_ from %d", this->get_modbus_name(), + this->power_demand_compensation_); + // only reduction of compensation but keep old demand: + if (this->power_demand_compensation_ != 0 && ((importing_now > 0 && power_demand < last_power_demand) || + (importing_now < 0 && power_demand > last_power_demand))) { + this->power_demand_compensation_ += power_demand - last_power_demand; + power_demand = last_power_demand; + ESP_LOGD(TAG, "'%s': Oscillation prevention, keeping previous demand: %d; reducing compensation", + this->get_modbus_name(), last_power_demand); + } else { + int16_t next_demand_compensation = this->power_demand_compensation_ + power_demand - last_power_demand; + // if not really compensating at the moment and demand changes a bit more, update current time stamp: helps to + // reduce false time-outs + if (abs(this->power_demand_compensation_) <= 15 && abs(next_demand_compensation) > 30) { + ESP_LOGD(TAG, "'%s': resetting only timeout: power_demand_compensation_: %d; next_demand_compensation: %d", + this->get_modbus_name(), this->power_demand_compensation_, next_demand_compensation); + this->power_demand_compensation_timestamp_ = millis(); + } + this->power_demand_compensation_ = next_demand_compensation; } + ESP_LOGD(TAG, "'%s': updated power_demand_compensation_ to %d", this->get_modbus_name(), + this->power_demand_compensation_); return power_demand; } @@ -201,5 +232,19 @@ void SoyosourceVirtualMeter::publish_state_(text_sensor::TextSensor *text_sensor text_sensor->publish_state(state); } +void SoyosourceVirtualMeter::reset_power_demand_compensation_(int16_t importing_now) { + // reset on zero crossing or timeout + if (importing_now == 0 || (this->power_demand_compensation_ > 0 && importing_now < 0) || + (this->power_demand_compensation_ < 0 && importing_now > 0) || + this->power_demand_compensation_timestamp_ + this->power_demand_compensation_timeout_ms_ < millis()) { + ESP_LOGD(TAG, "'%s': reset power_demand_compensation_ to 0 %s", this->get_modbus_name(), + this->power_demand_compensation_timestamp_ + this->power_demand_compensation_timeout_ms_ < millis() + ? "after timout" + : "after zero crossing"); + this->power_demand_compensation_timestamp_ = millis(); + this->power_demand_compensation_ = 0; + } +} + } // namespace soyosource_virtual_meter } // namespace esphome diff --git a/components/soyosource_virtual_meter/soyosource_virtual_meter.h b/components/soyosource_virtual_meter/soyosource_virtual_meter.h index 89bfb51..b6dfaa4 100644 --- a/components/soyosource_virtual_meter/soyosource_virtual_meter.h +++ b/components/soyosource_virtual_meter/soyosource_virtual_meter.h @@ -5,7 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/switch/switch.h" #include "esphome/components/text_sensor/text_sensor.h" -#include "esphome/components/soyosource_modbus/soyosource_modbus.h" +#include "../soyosource_modbus/soyosource_modbus.h" namespace esphome { namespace soyosource_virtual_meter { @@ -47,6 +47,12 @@ class SoyosourceVirtualMeter : public PollingComponent, public soyosource_modbus void set_power_demand_divider_number(number::Number *power_demand_divider_number) { power_demand_divider_number_ = power_demand_divider_number; } + void set_max_power_sensor_latency_ms_number(number::Number *max_power_sensor_latency_ms_number) { + this->power_demand_compensation_timeout_ms_number_ = max_power_sensor_latency_ms_number; + } + void set_power_demand_compensation_timeout_ms(uint16_t power_demand_compensation_timeout_ms) { + this->power_demand_compensation_timeout_ms_ = power_demand_compensation_timeout_ms; + } void set_manual_mode_switch(switch_::Switch *manual_mode_switch) { manual_mode_switch_ = manual_mode_switch; } void set_emergency_power_off_switch(switch_::Switch *emergency_power_off_switch) { @@ -73,6 +79,7 @@ class SoyosourceVirtualMeter : public PollingComponent, public soyosource_modbus number::Number *manual_power_demand_number_; number::Number *max_power_demand_number_; number::Number *power_demand_divider_number_; + number::Number *power_demand_compensation_timeout_ms_number_; sensor::Sensor *power_sensor_; sensor::Sensor *operation_status_sensor_; @@ -94,6 +101,10 @@ class SoyosourceVirtualMeter : public PollingComponent, public soyosource_modbus uint32_t last_power_demand_received_{0}; uint16_t last_power_demand_{0}; + int16_t power_demand_compensation_{0}; + uint32_t power_demand_compensation_timestamp_; + uint16_t power_demand_compensation_timeout_ms_{5000}; + void publish_state_(sensor::Sensor *sensor, float value); void publish_state_(text_sensor::TextSensor *text_sensor, const std::string &state); bool inactivity_timeout_(); @@ -102,6 +113,7 @@ class SoyosourceVirtualMeter : public PollingComponent, public soyosource_modbus int16_t calculate_power_demand_negative_measurements_(int16_t consumption, uint16_t last_power_demand); int16_t calculate_power_demand_restart_on_crossing_zero_(int16_t consumption, uint16_t last_power_demand); int16_t calculate_power_demand_oem_(int16_t consumption); + void reset_power_demand_compensation_(int16_t importing_now); }; } // namespace soyosource_virtual_meter