Skip to content
17 changes: 17 additions & 0 deletions components/soyosource_virtual_meter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(
Expand Down
32 changes: 32 additions & 0 deletions components/soyosource_virtual_meter/number/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand All @@ -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_(
Expand Down Expand Up @@ -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,
),
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
51 changes: 48 additions & 3 deletions components/soyosource_virtual_meter/soyosource_virtual_meter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
14 changes: 13 additions & 1 deletion components/soyosource_virtual_meter/soyosource_virtual_meter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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_;
Expand All @@ -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_();
Expand All @@ -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
Expand Down