From 60013fbe551611d9391036b8d490deb2884bfbd7 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 7 Aug 2026 12:04:43 +0200 Subject: [PATCH 1/3] usage type self controlled --- .../sample_consumer/consumer.py | 12 +++++++++- packages/control/consumer/consumer.py | 3 ++- packages/control/consumer/consumer_data.py | 22 +++++++------------ packages/control/consumer/usage.py | 4 ++++ packages/control/process.py | 20 ++++++++++++++--- packages/helpermodules/setdata.py | 10 ++++++--- packages/modules/common/abstract_consumer.py | 11 ++++++++++ .../modules/common/configurable_consumer.py | 7 ++++++ .../modules/consumers/generic/mqtt/config.py | 1 + .../consumers/generic/mqtt/consumer.py | 11 ++++++++++ .../source/src/stores/mqtt-store-model.ts | 1 + 11 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 packages/modules/common/abstract_consumer.py diff --git a/docs/samples/sample_consumer/sample_consumer/consumer.py b/docs/samples/sample_consumer/sample_consumer/consumer.py index a5a9cbd4e1..af25c43365 100644 --- a/docs/samples/sample_consumer/sample_consumer/consumer.py +++ b/docs/samples/sample_consumer/sample_consumer/consumer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from enum import IntEnum import logging +from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState from modules.common.configurable_consumer import ConfigurableConsumer @@ -52,12 +53,14 @@ def update() -> ConsumerState: temperatures=[resp[Register.TEMP0]/10, resp[Register.TEMP1]/10, resp[Register.TEMP2]/10] ) + # ConsumerUsage.SUSPENDABLE_TUNABLE def set_limit(power_limit: float) -> None: nonlocal client client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) # ODER - + # ConsumerUsage.SUSPENDABLE_ONOFF + # ConsumerUsage.CONTINUOUS def switch_on() -> None: nonlocal client client.write_registers(1000, 1, unit=config.configuration.modbus_id) @@ -65,10 +68,17 @@ def switch_on() -> None: def switch_off() -> None: nonlocal client client.write_registers(1000, 0, unit=config.configuration.modbus_id) + + # ODER + # ConsumerUsage.SELF_CONTROLLED + def send_values(values: CurrentValues) -> None: + client.write_registers(1000, values.evu_power, unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, update=update, + send_values=send_values, set_power_limit=set_limit, switch_on=switch_on, switch_off=switch_off) diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index 5ec2585569..a1291f3c3a 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -3,6 +3,7 @@ from typing import Callable, Dict, List, Optional, Tuple from control import data +from control.consumer.usage import NOT_CONTROLLED from control.algorithm.utils import get_medium_charging_current from control.chargemode import Chargemode from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState @@ -42,7 +43,7 @@ def setup_values_at_start(self): def update(self): try: self.setup_values_at_start() - if self.data.usage.type == ConsumerUsage.METER_ONLY: + if self.data.usage.type in NOT_CONTROLLED: return else: if self.data.get.voltages is None: diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index 584bd441b0..132b9184f2 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -46,11 +46,6 @@ class ResetChargemode: chargemode: Chargemode = Chargemode.INSTANT_CHARGING -@dataclass -class Log: - on_time: float = 0 - - @dataclass class Usage: chargemode: Chargemode = Chargemode.INSTANT_CHARGING @@ -93,18 +88,17 @@ class Get: @dataclass class Set: current: float = field(default=0, metadata={"topic": "set/current"}) - loadmanagement_available: bool = False + loadmanagement_available: bool = field(default=False) phases_to_use: int = field(default=1, metadata={"topic": "set/phases_to_use"}) plug_time: Optional[float] = field(default=None, metadata={"topic": "set/plug_time"}) - required_power: float = 0 - current_prev: float = 0 - state_str_prev: str = "" - target_current: float = 0 - charge_state_prev: bool = False - log: Log = field(default_factory=lambda: Log()) + required_power: float = field(default=0) + current_prev: float = field(default=0) + state_str_prev: str = field(default="") + target_current: float = field(default=0) + charge_state_prev: bool = field(default=False) on_time: float = field(default=0, metadata={"topic": "set/on_time"}) - power: Optional[float] = None - switch_interval_elapsed: bool = False + power: Optional[float] = field(default=None) + switch_interval_elapsed: bool = field(default=False) timestamp_last_current_set: float = field(default=0, metadata={"topic": "set/timestamp_last_current_set"}) wait_for_start_state: WaitForStartStates = field( default=WaitForStartStates.WAIT_FOR_DEVICE_START, metadata={"topic": "set/wait_for_start_state"}) diff --git a/packages/control/consumer/usage.py b/packages/control/consumer/usage.py index 167215a9e7..3bb50e0166 100644 --- a/packages/control/consumer/usage.py +++ b/packages/control/consumer/usage.py @@ -3,6 +3,10 @@ class ConsumerUsage(Enum): METER_ONLY = "meter_only" + SELF_CONTROLLED = "self_controlled" SUSPENDABLE_TUNABLE = "suspendable_tunable" SUSPENDABLE_ONOFF = "suspendable_onoff" CONTINUOUS = "continuous" + + +NOT_CONTROLLED = (ConsumerUsage.METER_ONLY, ConsumerUsage.SELF_CONTROLLED) diff --git a/packages/control/process.py b/packages/control/process.py index 8269a9385b..e5c7c8c144 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -15,6 +15,7 @@ from helpermodules.phase_handling import voltages_mean from helpermodules.pub import Pub from helpermodules.utils._thread_handler import joined_thread_handler +from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_io import AbstractIoDevice from modules.common.configurable_device import set_power_limit_wrapper from modules.common.fault_state_level import FaultStateLevel @@ -187,6 +188,19 @@ def _start_consumer(self, consumer: Consumer) -> Thread: return Thread( target=consumer.module.switch_on if consumer.data.set.current > 0 else consumer.module.switch_off, name=f"set current consumer{consumer.num}") - return Thread(target=consumer.module.set_power_limit, - args=(consumer.data.set.power,), - name=f"set current consumer{consumer.num}") + elif consumer.data.usage.type == ConsumerUsage.SELF_CONTROLLED: + current_values = CurrentValues( + bat_power=data.data.bat_all_data.data.get.power, + bat_soc=data.data.bat_all_data.data.get.soc, + cp_power=data.data.cp_all_data.data.get.power, + evu_power=data.data.counter_all_data.get_evu_counter().data.get.power, + home_consumption=data.data.counter_all_data.data.set.home_consumption, + pv_power=data.data.pv_all_data.data.get.power + ) + return Thread(target=consumer.module.send_values, + args=(current_values,), + name=f"set current consumer{consumer.num}") + else: + return Thread(target=consumer.module.set_power_limit, + args=(consumer.data.set.power,), + name=f"set current consumer{consumer.num}") diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 0629c386fc..00d222c0cb 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -861,7 +861,9 @@ def process_mqtt_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/mqtt/consumer/" in msg.topic: if re.search("openWB/set/mqtt/consumer/[0-9]+/set/switch", msg.topic) is not None: self._validate_value(msg, bool) - elif re.search("openWB/set/mqtt/consumer/[0-9]+/set/power", msg.topic) is not None: + elif re.search("openWB/set/mqtt/consumer/[0-9]+/set/" + + "(power|bat_power|bat_soc|cp_power|evu_power|home_consumption|pv_power)", + msg.topic) is not None: self._validate_value(msg, float) else: self.process_consumer_topic(msg) @@ -1270,9 +1272,11 @@ def process_consumer_topic(self, msg): re.search("consumer/[0-9]+/get/daily_imported$", msg.topic) is not None or re.search("consumer/[0-9]+/get/timestamp_last_current_set$", msg.topic) is not None): self._validate_value(msg, float, [(0, float("inf"))]) - elif re.search("consumer/[0-9]+/get/fault_state$", msg.topic) is not None: + elif (re.search("consumer/[0-9]+/get/fault_state$", msg.topic) is not None or + re.search("consumer/get/fault_state$", msg.topic) is not None): self._validate_value(msg, int, [(0, 2)]) - elif (re.search("consumer/[0-9]+/get/fault_str$", msg.topic) is not None or + elif (re.search("consumer/get/fault_str$", msg.topic) is not None or + re.search("consumer/[0-9]+/get/fault_str$", msg.topic) is not None or re.search("consumer/[0-9]+/get/state_str$", msg.topic) is not None or re.search("consumer/[0-9]+/set/wait_for_start_state$", msg.topic) is not None): self._validate_value(msg, str) diff --git a/packages/modules/common/abstract_consumer.py b/packages/modules/common/abstract_consumer.py new file mode 100644 index 0000000000..dd6f423c8e --- /dev/null +++ b/packages/modules/common/abstract_consumer.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class CurrentValues: + bat_power: float = 0 + bat_soc: float = 0 + cp_power: float = 0 + evu_power: float = 0 + home_consumption: float = 0 + pv_power: float = 0 diff --git a/packages/modules/common/configurable_consumer.py b/packages/modules/common/configurable_consumer.py index 4c3ae2978e..bc1200b769 100644 --- a/packages/modules/common/configurable_consumer.py +++ b/packages/modules/common/configurable_consumer.py @@ -3,6 +3,7 @@ from helpermodules import timecheck from helpermodules.pub import Pub +from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import AbstractCounter from modules.common.component_context import SingleComponentUpdateContext from modules.common.component_state import ConsumerState @@ -24,6 +25,7 @@ def __init__(self, initializer: Optional[Callable] = lambda: None, error_handler: Optional[Callable] = lambda: None, update: Optional[Callable[[], ConsumerState]] = lambda: None, + send_values: Optional[Callable[[CurrentValues], None]] = lambda: None, set_power_limit: Optional[Callable] = lambda: None, switch_on: Optional[Callable] = lambda: None, switch_off: Optional[Callable] = lambda: None,) -> None: @@ -31,6 +33,7 @@ def __init__(self, self.module_initializer = initializer self.module_error_handler = error_handler self.module_updater = update + self.module_send_values = send_values self.module_set_power_limit = set_power_limit self.module_switch_on = switch_on self.module_switch_off = switch_off @@ -88,6 +91,10 @@ def switch_off(self) -> None: with SingleComponentUpdateContext(self.fault_state): self.module_switch_off() + def send_values(self, values: CurrentValues) -> None: + with SingleComponentUpdateContext(self.fault_state): + self.module_send_values(values) + def dependency_injection_devices_components(component: AbstractCounter): component.sim_counter.component_type = ComponentType.CONSUMER.value diff --git a/packages/modules/consumers/generic/mqtt/config.py b/packages/modules/consumers/generic/mqtt/config.py index b2ab560af1..39e2daa961 100644 --- a/packages/modules/consumers/generic/mqtt/config.py +++ b/packages/modules/consumers/generic/mqtt/config.py @@ -21,6 +21,7 @@ def __init__(self, configuration: MqttConfiguration = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, ConsumerUsage.CONTINUOUS, + ConsumerUsage.SELF_CONTROLLED, ConsumerUsage.SUSPENDABLE_ONOFF, ConsumerUsage.SUSPENDABLE_TUNABLE), **kwargs) -> None: diff --git a/packages/modules/consumers/generic/mqtt/consumer.py b/packages/modules/consumers/generic/mqtt/consumer.py index e81794dba3..37bf391815 100644 --- a/packages/modules/consumers/generic/mqtt/consumer.py +++ b/packages/modules/consumers/generic/mqtt/consumer.py @@ -5,6 +5,7 @@ from helpermodules.pub import Pub from helpermodules.utils._get_default import get_default from helpermodules.utils.topic_parser import decode_payload +from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState from modules.common.configurable_consumer import ConfigurableConsumer @@ -50,8 +51,18 @@ def switch_on() -> None: def switch_off() -> None: Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/switch", False) + + def send_values(values: CurrentValues) -> None: + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/bat_power", values.bat_power) + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/bat_soc", values.bat_soc) + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/cp_power", values.cp_power) + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/evu_power", values.evu_power) + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/home_consumption", values.home_consumption) + Pub().pub(f"openWB/set/mqtt/consumer/{config.id}/set/pv_power", values.pv_power) + return ConfigurableConsumer(consumer_config=config, update=update, + send_values=send_values, set_power_limit=set_power_limit, switch_on=switch_on, switch_off=switch_off) diff --git a/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts b/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts index ef78053057..c66c21aefa 100644 --- a/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts +++ b/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts @@ -145,6 +145,7 @@ export interface ConsumerModule { } export type ConsumerUsageType = | 'meter_only' + | 'self_controlled' | 'suspendable_tunable' | 'suspendable_onoff' | 'continuous'; From c4f53a73aa1d6bde54091fd41f6ee690942b5895 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 7 Aug 2026 12:12:31 +0200 Subject: [PATCH 2/3] fix --- .../web_themes/koala/source/src/components/ConsumerCard.vue | 2 +- .../web_themes/koala/source/src/components/ConsumerSettings.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue b/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue index f266c5ec7e..312b3d9aaf 100644 --- a/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue +++ b/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue @@ -123,7 +123,7 @@ const statusColor = computed(() => { }); const meterOnly = computed( - () => mqttStore.consumerUsageType(props.consumerId) === 'meter_only', + () => mqttStore.consumerUsageType(props.consumerId) === 'meter_only' || mqttStore.consumerUsageType(props.consumerId) === 'self_controlled', ); const consumerColor = computed(() => { diff --git a/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue b/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue index 1336551ced..b8d9773054 100644 --- a/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue +++ b/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue @@ -161,7 +161,7 @@ const consumerUsageType = computed(() => /** Meter-only consumers cannot be controlled, so hide the mode controls. */ const showModeControls = computed( - () => consumerUsageType.value !== 'meter_only', + () => consumerUsageType.value !== 'meter_only' && consumerUsageType.value !== 'self_controlled', ); From b83441ba59696496d527b0f52e33c8d500f8c78c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 7 Aug 2026 13:16:24 +0200 Subject: [PATCH 3/3] review --- .../koala/source/src/components/ConsumerCard.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue b/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue index 312b3d9aaf..33f01554fd 100644 --- a/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue +++ b/packages/modules/web_themes/koala/source/src/components/ConsumerCard.vue @@ -122,9 +122,10 @@ const statusColor = computed(() => { return isRunning.value ? 'positive' : 'grey-6'; }); -const meterOnly = computed( - () => mqttStore.consumerUsageType(props.consumerId) === 'meter_only' || mqttStore.consumerUsageType(props.consumerId) === 'self_controlled', -); +const meterOnly = computed(() => { + const usageType = mqttStore.consumerUsageType(props.consumerId); + return usageType === 'meter_only' || usageType === 'self_controlled'; +}); const consumerColor = computed(() => { return mqttStore.consumerColor(props.consumerId) || 'var(--q-consumer)';