From 49f5872af3261cd9dbd7246cea9ed00f953d7b37 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 14 Nov 2025 12:39:32 +0100 Subject: [PATCH 1/2] Dynamische Netzentgelte --- packages/control/ev/charge_template.py | 10 +- packages/control/ev/charge_template_test.py | 7 +- packages/control/optional.py | 183 ++++++++++-------- packages/control/optional_data.py | 46 ++++- packages/control/optional_test.py | 48 +++-- packages/helpermodules/create_debug.py | 2 +- .../measurement_logging/write_log.py | 3 +- packages/helpermodules/setdata.py | 27 ++- packages/helpermodules/subdata.py | 45 +++-- packages/helpermodules/update_config.py | 29 ++- packages/main.py | 3 +- packages/modules/common/component_type.py | 9 +- .../modules/common/configurable_tariff.py | 23 ++- .../common/configurable_tariff_test.py | 8 +- packages/modules/common/fault_state.py | 3 +- packages/modules/common/store/__init__.py | 2 +- packages/modules/common/store/_tariff.py | 98 +++++++++- packages/modules/common/store/_tariff_test.py | 66 +++++++ packages/modules/configuration.py | 74 +++---- .../display_themes/cards/source/src/App.vue | 4 +- .../cards/source/src/stores/mqtt.js | 7 +- .../src/components/priceChart/PriceChart.vue | 1 - .../components/priceChart/processMessages.ts | 5 +- .../__init__.py | 0 .../flexible_tariffs/awattar}/__init__.py | 0 .../flexible_tariffs}/awattar/config.py | 0 .../flexible_tariffs}/awattar/tariff.py | 4 +- .../flexible_tariffs}/awattar/tariff_test.py | 4 +- .../flexible_tariffs/ekz}/__init__.py | 0 .../flexible_tariffs}/ekz/config.py | 0 .../flexible_tariffs}/ekz/tariff.py | 8 +- .../energycharts}/__init__.py | 0 .../flexible_tariffs}/energycharts/config.py | 0 .../flexible_tariffs}/energycharts/tariff.py | 4 +- .../flexible_tariffs/fixed_hours}/__init__.py | 0 .../flexible_tariffs}/fixed_hours/config.py | 4 +- .../flexible_tariffs}/fixed_hours/tariff.py | 23 +-- .../flexible_tariffs/groupe_e}/__init__.py | 0 .../flexible_tariffs}/groupe_e/config.py | 0 .../flexible_tariffs}/groupe_e/tariff.py | 6 +- .../flexible_tariffs}/octopusenergy/config.py | 0 .../flexible_tariffs}/octopusenergy/tariff.py | 3 +- .../flexible_tariffs/ostrom}/__init__.py | 0 .../flexible_tariffs}/ostrom/config.py | 0 .../flexible_tariffs}/ostrom/tariff.py | 4 +- .../flexible_tariffs/rabot}/__init__.py | 0 .../flexible_tariffs}/rabot/config.py | 0 .../flexible_tariffs}/rabot/tariff.py | 6 +- .../flexible_tariffs/tibber}/__init__.py | 0 .../flexible_tariffs}/tibber/config.py | 0 .../flexible_tariffs}/tibber/tariff.py | 4 +- .../flexible_tariffs}/tibber/tariff_test.py | 6 +- .../flexible_tariffs/voltego/__init__.py | 0 .../flexible_tariffs}/voltego/config.py | 0 .../flexible_tariffs}/voltego/tariff.py | 4 +- .../grid_fees/fixed_hours/__init__.py | 0 .../grid_fees/fixed_hours/tariff.py | 12 ++ packages/modules/loadvars.py | 46 ++++- .../priceChart/GlobalPriceChart.vue | 1 - .../src/components/priceChart/PriceChart.vue | 1 - .../components/priceChart/processMessages.ts | 5 +- .../koala/source/src/stores/mqtt-store.ts | 12 +- .../modules/web_themes/koala/web/index.html | 2 +- packages/modules/web_themes/koala/web/sw.js | 2 +- .../web_themes/standard_legacy/web/index.html | 8 - .../standard_legacy/web/processAllMqttMsg.js | 9 +- .../standard_legacy/web/setupMqttServices.js | 4 +- 67 files changed, 588 insertions(+), 297 deletions(-) create mode 100644 packages/modules/common/store/_tariff_test.py rename packages/modules/{electricity_tariffs/awattar => electricity_pricing}/__init__.py (100%) rename packages/modules/{electricity_tariffs/ekz => electricity_pricing/flexible_tariffs/awattar}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/awattar/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/awattar/tariff.py (91%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/awattar/tariff_test.py (94%) rename packages/modules/{electricity_tariffs/energycharts => electricity_pricing/flexible_tariffs/ekz}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/ekz/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/ekz/tariff.py (89%) rename packages/modules/{electricity_tariffs/fixed_hours => electricity_pricing/flexible_tariffs/energycharts}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/energycharts/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/energycharts/tariff.py (91%) rename packages/modules/{electricity_tariffs/groupe_e => electricity_pricing/flexible_tariffs/fixed_hours}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/fixed_hours/config.py (90%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/fixed_hours/tariff.py (74%) rename packages/modules/{electricity_tariffs/ostrom => electricity_pricing/flexible_tariffs/groupe_e}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/groupe_e/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/groupe_e/tariff.py (88%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/octopusenergy/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/octopusenergy/tariff.py (96%) rename packages/modules/{electricity_tariffs/rabot => electricity_pricing/flexible_tariffs/ostrom}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/ostrom/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/ostrom/tariff.py (92%) rename packages/modules/{electricity_tariffs/tibber => electricity_pricing/flexible_tariffs/rabot}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/rabot/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/rabot/tariff.py (92%) rename packages/modules/{electricity_tariffs/voltego => electricity_pricing/flexible_tariffs/tibber}/__init__.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/tibber/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/tibber/tariff.py (92%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/tibber/tariff_test.py (95%) create mode 100644 packages/modules/electricity_pricing/flexible_tariffs/voltego/__init__.py rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/voltego/config.py (100%) rename packages/modules/{electricity_tariffs => electricity_pricing/flexible_tariffs}/voltego/tariff.py (95%) create mode 100644 packages/modules/electricity_pricing/grid_fees/fixed_hours/__init__.py create mode 100644 packages/modules/electricity_pricing/grid_fees/fixed_hours/tariff.py diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 0b216d4423..41c206528a 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -292,8 +292,8 @@ def eco_charging(self, current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED - elif data.data.optional_data.et_provider_available(): - if data.data.optional_data.et_is_charging_allowed_price_threshold(eco_charging.max_price): + elif data.data.optional_data.data.electricity_pricing.configured: + if data.data.optional_data.ep_is_charging_allowed_price_threshold(eco_charging.max_price): sub_mode = "instant_charging" message = self.CHARGING_PRICE_LOW phases = max_phases_hw @@ -608,7 +608,7 @@ def end_of_today_timestamp() -> int: hour=23, minute=59, second=59, microsecond=999000).timestamp() def is_loading_hour(hour: int) -> bool: - return data.data.optional_data.et_is_charging_allowed_hours_list(hour) + return data.data.optional_data.ep_is_charging_allowed_hours_list(hour) def convert_loading_hours_to_string(hour_list: List[int]) -> str: if 1 < len(hour_list): @@ -639,11 +639,11 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: else '') return loading_message + '.' - hour_list = data.data.optional_data.et_get_loading_hours( + hour_list = data.data.optional_data.ep_get_loading_hours( selected_plan.duration, selected_plan.duration + selected_plan.remaining_time) log.debug(f"Günstige Ladezeiten: {hour_list}") - if data.data.optional_data.et_is_charging_allowed_hours_list(hour_list): + if data.data.optional_data.ep_is_charging_allowed_hours_list(hour_list): message = self.SCHEDULED_CHARGING_CHEAP_HOUR.format(get_hours_message()) current = plan_current submode = "instant_charging" diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 03866a4cdc..3cf5bb1437 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -88,7 +88,6 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou def test_instant_charging(selected: str, current_soc: float, used_amount: float, expected: Tuple[int, str, Optional[str]]): # setup - data.data.optional_data.data.et.active = False ct = ChargeTemplate() ct.data.chargemode.instant_charging.limit.selected = selected ct.data.chargemode.instant_charging.limit.amount = 1000 @@ -377,10 +376,10 @@ def test_scheduled_charging_calc_current_electricity_tariff( plan.limit.selected = "soc" ct.data.chargemode.scheduled_charging.plans = [plan] # für Github-Test keinen Zeitstempel verwenden - mock_et_get_loading_hours = Mock(return_value=loading_hours) - monkeypatch.setattr(data.data.optional_data, "et_get_loading_hours", mock_et_get_loading_hours) + mock_ep_get_loading_hours = Mock(return_value=loading_hours) + monkeypatch.setattr(data.data.optional_data, "ep_get_loading_hours", mock_ep_get_loading_hours) mock_is_list_valid = Mock(return_value=is_loading_hour) - monkeypatch.setattr(data.data.optional_data, "et_is_charging_allowed_hours_list", mock_is_list_valid) + monkeypatch.setattr(data.data.optional_data, "ep_is_charging_allowed_hours_list", mock_is_list_valid) # execution ret = ct.scheduled_charging_calc_current( diff --git a/packages/control/optional.py b/packages/control/optional.py index b112192b49..e881567bdb 100644 --- a/packages/control/optional.py +++ b/packages/control/optional.py @@ -4,18 +4,18 @@ from math import ceil import random from threading import Thread -from typing import List, Optional as TypingOptional +from typing import List, Optional as TypingOptional, Union from datetime import datetime, timedelta from control import data from control.ocpp import OcppMixin -from control.optional_data import OptionalData +from control.optional_data import FlexibleTariff, GridFee, OptionalData, PricingGet from helpermodules import hardware_configuration from helpermodules.constants import NO_ERROR from helpermodules.pub import Pub from helpermodules import timecheck from helpermodules.utils import thread_handler -from modules.common.configurable_tariff import ConfigurableElectricityTariff +from modules.common.configurable_tariff import ConfigurableFlexibleTariff, ConfigurableGridFee from modules.common.configurable_monitoring import ConfigurableMonitoring log = logging.getLogger(__name__) @@ -27,36 +27,63 @@ class Optional(OcppMixin): def __init__(self): try: self.data = OptionalData() - # guarded et_module stored in a private attribute - self._et_module: TypingOptional[ConfigurableElectricityTariff] = None - self.monitoring_module: ConfigurableMonitoring = None + self._flexible_tariff_module: TypingOptional[ConfigurableFlexibleTariff] = None + self._grid_fee_module: TypingOptional[ConfigurableGridFee] = None + self.monitoring_module: TypingOptional[ConfigurableMonitoring] = None self.data.dc_charging = hardware_configuration.get_hardware_configuration_setting("dc_charging") Pub().pub("openWB/optional/dc_charging", self.data.dc_charging) except Exception: log.exception("Fehler im Optional-Modul") @property - def et_module(self) -> TypingOptional[ConfigurableElectricityTariff]: - """Getter for the electricity tariff module (may be None).""" - return self._et_module + def flexible_tariff_module(self) -> TypingOptional[ConfigurableFlexibleTariff]: + return self._flexible_tariff_module - @et_module.setter - def et_module(self, value: TypingOptional[ConfigurableElectricityTariff]): - """Setter with basic type-guarding and logging. + @flexible_tariff_module.setter + def flexible_tariff_module(self, value: TypingOptional[ConfigurableFlexibleTariff]): + if (value is None or + (self._flexible_tariff_module and value and + self._flexible_tariff_module.config.name != value.config.name)): + self.data.electricity_pricing.flexible_tariff.get = PricingGet() + self._reset_state(self.data.electricity_pricing.flexible_tariff, "flexible_tariff") + self._flexible_tariff_module = value + if value: + self.data.electricity_pricing.get.next_query_time = None + Pub().pub("openWB/set/optional/ep/get/next_query_time", None) + self._set_ep_configured() - Accepts either None or a ConfigurableElectricityTariff instance. Logs when set/cleared. - """ - if self._et_module and (value is None or self._et_module.config.name == value.config.name): - log.debug("Replacing existing et_module on Optional not allowed!") + @property + def grid_fee_module(self) -> TypingOptional[ConfigurableGridFee]: + return self._grid_fee_module + + @grid_fee_module.setter + def grid_fee_module(self, value: TypingOptional[ConfigurableGridFee]): + if (value is None or + (self._grid_fee_module and value and self._grid_fee_module.config.name != value.config.name)): + self.data.electricity_pricing.grid_fee.get = PricingGet() + self._reset_state(self.data.electricity_pricing.grid_fee, "grid_fee") + self._grid_fee_module = value + if value: + self.data.electricity_pricing.get.next_query_time = None + Pub().pub("openWB/set/optional/ep/get/next_query_time", None) + self._set_ep_configured() + + def _set_ep_configured(self): + if self._grid_fee_module or self._flexible_tariff_module: + self.data.electricity_pricing.configured = True + Pub().pub("openWB/set/optional/ep/configured", True) else: - if value is not None and not isinstance(value, ConfigurableElectricityTariff): - raise TypeError("et_module must be a ConfigurableElectricityTariff instance or None") - self._et_module = value - if value is not None: - log.info("et_module set on Optional: %s", value.config.name) - self.et_get_prices() - else: - log.info("et_module cleared in Optional") + self.data.electricity_pricing.configured = False + Pub().pub("openWB/set/optional/ep/configured", False) + + def _reset_state(self, module: Union[FlexibleTariff, GridFee], module_name: str): + if (module.get.fault_state != 0 or module.get.fault_str != NO_ERROR): + module.get.fault_state = 0 + module.get.fault_str = NO_ERROR + Pub().pub(f"openWB/set/optional/ep/{module_name}/get/fault_state", 0) + Pub().pub(f"openWB/set/optional/ep/{module_name}/get/fault_str", NO_ERROR) + Pub().pub(f"openWB/set/optional/ep/{module_name}/get/prices", {}) + Pub().pub("openWB/set/optional/ep/get/prices", {}) def monitoring_start(self): if self.monitoring_module is not None: @@ -66,10 +93,7 @@ def monitoring_stop(self): if self.monitoring_module is not None: self.monitoring_module.stop_monitoring() - def et_provider_available(self) -> bool: - return self.et_module is not None - - def et_is_charging_allowed_hours_list(self, selected_hours: list[int]) -> bool: + def ep_is_charging_allowed_hours_list(self, selected_hours: list[int]) -> bool: """ prüft, ob das strompreisbasiertes Laden aktiviert und ein günstiger Zeitpunkt ist. Parameter @@ -83,8 +107,8 @@ def et_is_charging_allowed_hours_list(self, selected_hours: list[int]) -> bool: False: Der aktuelle Zeitpunkt liegt in keinem günstigen Zeitslot """ try: - if self.et_provider_available(): - return self.__get_current_timeslot_start(self.data.et.get.prices) in selected_hours + if self.data.electricity_pricing.configured: + return self.__get_current_timeslot_start() in selected_hours else: log.info("Prüfe strompreisbasiertes Laden: Nicht konfiguriert") return False @@ -92,7 +116,7 @@ def et_is_charging_allowed_hours_list(self, selected_hours: list[int]) -> bool: log.exception(f"Fehler im Optional-Modul: {e}") return False - def et_is_charging_allowed_price_threshold(self, max_price: float) -> bool: + def ep_is_charging_allowed_price_threshold(self, max_price: float) -> bool: """ prüft, ob der aktuelle Strompreis niedriger oder gleich der festgelegten Preisgrenze ist. Return @@ -101,8 +125,8 @@ def et_is_charging_allowed_price_threshold(self, max_price: float) -> bool: False: Preis liegt darüber """ try: - if self.et_provider_available(): - current_price = self.et_get_current_price(prices=self.data.et.get.prices) + if self.data.electricity_pricing.configured: + current_price = self.ep_get_current_price() log.info("Prüfe strompreisbasiertes Laden mit Preisgrenze %.5f €/kWh, aktueller Preis: %.5f €/kWh", max_price * AS_EURO_PER_KWH, current_price * AS_EURO_PER_KWH @@ -117,39 +141,39 @@ def et_is_charging_allowed_price_threshold(self, max_price: float) -> bool: log.exception("Fehler im Optional-Modul: %s", e) return False - def __get_first_entry(self, prices: dict[str, float]) -> tuple[str, float]: - if self.et_provider_available(): - prices = self.data.et.get.prices + def __get_first_entry(self) -> tuple[str, float]: + if self.data.electricity_pricing.configured: + prices = self.data.electricity_pricing.get.prices if prices is None or len(prices) == 0: raise Exception("Keine Preisdaten für strompreisbasiertes Laden vorhanden.") else: timestamp, first = next(iter(prices.items())) price_timeslot_seconds = self.__calculate_price_timeslot_length(prices) - now = int(timecheck.create_timestamp()) + now = timecheck.create_timestamp() prices = { price[0]: price[1] for price in prices.items() - if int(price[0]) > now - (price_timeslot_seconds - 1) + if float(price[0]) > now - (price_timeslot_seconds - 1) } - self.data.et.get.prices = prices + self.data.electricity_pricing.get.prices = prices timestamp, first = next(iter(prices.items())) return timestamp, first else: raise Exception("Kein Anbieter für strompreisbasiertes Laden konfiguriert.") - def __get_current_timeslot_start(self, prices: dict[str, float]) -> int: - timestamp, first = self.__get_first_entry(prices) - return int(timestamp) + def __get_current_timeslot_start(self) -> int: + timestamp = self.__get_first_entry()[0] + return float(timestamp) - def et_get_current_price(self, prices: dict[str, float]) -> float: - timestamp, first = self.__get_first_entry(prices) + def ep_get_current_price(self) -> float: + first = self.__get_first_entry()[1] return first def __calculate_price_timeslot_length(self, prices: dict) -> int: first_timestamps = list(prices.keys())[:2] - return int(first_timestamps[1]) - int(first_timestamps[0]) + return float(first_timestamps[1]) - float(first_timestamps[0]) - def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[int]: + def ep_get_loading_hours(self, duration: float, remaining_time: float) -> List[int]: """ Parameter --------- @@ -161,20 +185,20 @@ def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[i ------ list: Key des Dictionary (Unix-Sekunden der günstigen Zeit-Slots) """ - if self.et_provider_available() is False: + if self.data.electricity_pricing.configured is False: raise Exception("Kein Anbieter für strompreisbasiertes Laden konfiguriert.") try: - prices = self.data.et.get.prices + prices = self.data.electricity_pricing.get.prices price_timeslot_seconds = self.__calculate_price_timeslot_length(prices) - now = int(timecheck.create_timestamp()) + now = timecheck.create_timestamp() price_candidates = { timestamp: price for timestamp, price in prices.items() if ( # is current timeslot or futur - int(timestamp) + price_timeslot_seconds > now and + float(timestamp) + price_timeslot_seconds > now and # ends before plan target time - not int(timestamp) >= now + remaining_time + not float(timestamp) >= now + remaining_time ) } log.debug("%s Preis-Kandidaten in %s Sekunden zwischen %s Uhr und %s Uhr von %s Uhr bis %s Uhr", @@ -182,15 +206,15 @@ def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[i duration, datetime.fromtimestamp(now), datetime.fromtimestamp(now + remaining_time), - datetime.fromtimestamp(int(min(price_candidates))), - datetime.fromtimestamp(int(max(price_candidates))+price_timeslot_seconds)) + datetime.fromtimestamp(float(min(price_candidates))), + datetime.fromtimestamp(float(max(price_candidates))+price_timeslot_seconds)) ordered_by_date_reverse = reversed(sorted(price_candidates.items(), key=lambda x: x[0])) ordered_by_price = sorted(ordered_by_date_reverse, key=lambda x: x[1]) - selected_time_slots = {int(i[0]): float(i[1]) + selected_time_slots = {float(i[0]): float(i[1]) for i in ordered_by_price[:1 + ceil(duration/price_timeslot_seconds)]} selected_lenght = ( price_timeslot_seconds * (len(selected_time_slots)-1) - - (int(now) - min(selected_time_slots)) + (float(now) - min(selected_time_slots)) ) return sorted(selected_time_slots.keys() if not (min(selected_time_slots) > now or duration <= selected_lenght) @@ -201,24 +225,9 @@ def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[i log.exception("Fehler im Optional-Modul: %s", e) return [] - def et_get_prices(self): - try: - if self.et_module and self.et_price_update_required(): - thread_handler(Thread(target=self.et_module.update, args=(), - name="electricity tariff in optional")) - self.data.et.get.next_query_time = None - Pub().pub("openWB/set/optional/et/get/next_query_time", None) - else: - # Wenn kein Modul konfiguriert ist, Fehlerstatus zurücksetzen. - if self.data.et.get.fault_state != 0 or self.data.et.get.fault_str != NO_ERROR: - Pub().pub("openWB/set/optional/et/get/fault_state", 0) - Pub().pub("openWB/set/optional/et/get/fault_str", NO_ERROR) - except Exception as e: - log.exception("Fehler im Optional-Modul: %s", e) - def et_price_update_required(self) -> bool: def is_tomorrow(last_timestamp: str) -> bool: - return (day_of(date=datetime.now()) < day_of(datetime.fromtimestamp(int(last_timestamp))) + return (day_of(date=datetime.now()) < day_of(datetime.fromtimestamp(float(last_timestamp))) or day_of(date=datetime.now()).hour < TARIFF_UPDATE_HOUR) def day_of(date: datetime) -> datetime: @@ -226,31 +235,35 @@ def day_of(date: datetime) -> datetime: def get_last_entry_time_stamp() -> str: last_known_timestamp = "0" - if self.data.et.get.prices is not None: - last_known_timestamp = max(self.data.et.get.prices) + if self.data.electricity_pricing.get.prices is not None: + last_known_timestamp = max(self.data.electricity_pricing.get.prices) return last_known_timestamp - if len(self.data.et.get.prices) == 0: + self._set_ep_configured() + if self.data.electricity_pricing.configured is False: + return False + if len(self.data.electricity_pricing.get.prices) == 0: return True - if self.data.et.get.next_query_time is None: - next_query_time = datetime.fromtimestamp(int(max(self.data.et.get.prices))).replace( + if self.data.electricity_pricing.get.next_query_time is None: + next_query_time = datetime.fromtimestamp(float(max(self.data.electricity_pricing.get.prices))).replace( hour=TARIFF_UPDATE_HOUR, minute=0, second=0 ) + timedelta( # aktually ET providers issue next day prices up to half an hour earlier then 14:00 # reduce serverload on their site by trying early and randomizing query time minutes=random.randint(1, 7) * -5 ) - self.data.et.get.next_query_time = next_query_time.timestamp() - Pub().pub("openWB/set/optional/et/get/next_query_time", self.data.et.get.next_query_time) + self.data.electricity_pricing.get.next_query_time = next_query_time.timestamp() + Pub().pub("openWB/set/optional/ep/get/next_query_time", self.data.electricity_pricing.get.next_query_time) + return True if is_tomorrow(get_last_entry_time_stamp()): - if timecheck.create_timestamp() > self.data.et.get.next_query_time: - log.info( - f'Wartezeit {datetime.fromtimestamp(self.data.et.get.next_query_time).strftime("%Y%m%d-%H:%M:%S")}' - ' abgelaufen, Strompreise werden abgefragt') + if timecheck.create_timestamp() > self.data.electricity_pricing.get.next_query_time: + next_query_formatted = datetime.fromtimestamp( + self.data.electricity_pricing.get.next_query_time).strftime("%Y%m%d-%H:%M:%S") + log.info(f'Wartezeit {next_query_formatted} abgelaufen, Strompreise werden abgefragt') return True else: - log.info( - 'Nächster Abruf der Strompreise ' - f'{datetime.fromtimestamp(self.data.et.get.next_query_time).strftime("%Y%m%d-%H:%M:%S")}.') + next_query_formatted = datetime.fromtimestamp( + self.data.electricity_pricing.get.next_query_time).strftime("%Y%m%d-%H:%M:%S") + log.info(f'Nächster Abruf der Strompreise {next_query_formatted}.') return False return False diff --git a/packages/control/optional_data.py b/packages/control/optional_data.py index a0757fe623..0f96ff0ec0 100644 --- a/packages/control/optional_data.py +++ b/packages/control/optional_data.py @@ -7,24 +7,54 @@ @dataclass -class EtGet: +class PricingGet: fault_state: int = 0 fault_str: str = NO_ERROR + prices: Dict = field(default_factory=empty_dict_factory) + + +def get_factory() -> PricingGet: + return PricingGet() + + +@dataclass +class FlexibleTariff: + get: PricingGet = field(default_factory=get_factory) + + +def get_flexible_tariff_factory() -> FlexibleTariff: + return FlexibleTariff() + + +@dataclass +class GridFee: + get: PricingGet = field(default_factory=get_factory) + + +def get_grid_fee_factory() -> GridFee: + return GridFee() + + +@dataclass +class ElectricityPricingGet: next_query_time: Optional[float] = None prices: Dict = field(default_factory=empty_dict_factory) -def get_factory() -> EtGet: - return EtGet() +def electricity_pricing_get_factory() -> ElectricityPricingGet: + return ElectricityPricingGet() @dataclass -class Et: - get: EtGet = field(default_factory=get_factory) +class ElectricityPricing: + configured: bool = False + flexible_tariff: FlexibleTariff = field(default_factory=get_flexible_tariff_factory) + grid_fee: GridFee = field(default_factory=get_grid_fee_factory) + get: ElectricityPricingGet = field(default_factory=electricity_pricing_get_factory) -def et_factory() -> Et: - return Et() +def ep_factory() -> ElectricityPricing: + return ElectricityPricing() @dataclass @@ -84,7 +114,7 @@ def ocpp_factory() -> Ocpp: @dataclass class OptionalData: - et: Et = field(default_factory=et_factory) + electricity_pricing: ElectricityPricing = field(default_factory=ep_factory) int_display: InternalDisplay = field(default_factory=int_display_factory) led: Led = field(default_factory=led_factory) rfid: Rfid = field(default_factory=rfid_factory) diff --git a/packages/control/optional_test.py b/packages/control/optional_test.py index ac3ca3e28d..b1d9d516cc 100644 --- a/packages/control/optional_test.py +++ b/packages/control/optional_test.py @@ -1,7 +1,8 @@ from unittest.mock import Mock -from control.optional import Optional -from helpermodules import timecheck import pytest +from helpermodules import timecheck +from control.optional import Optional + ONE_HOUR_SECONDS = 3600 IGNORED = 0.0001 @@ -220,7 +221,7 @@ ), ], ) -def test_et_get_loading_hours(granularity, +def test_ep_get_loading_hours(granularity, now_ts, duration, remaining_time, @@ -229,9 +230,8 @@ def test_et_get_loading_hours(granularity, monkeypatch): # setup opt = Optional() - opt.data.et.get.prices = price_list - mock_et_provider_available = Mock(return_value=True) - monkeypatch.setattr(opt, "et_provider_available", mock_et_provider_available) + opt.data.electricity_pricing.get.prices = price_list + opt.data.electricity_pricing.configured = True monkeypatch.setattr( timecheck, "create_timestamp", @@ -239,7 +239,7 @@ def test_et_get_loading_hours(granularity, ) # execution - loading_hours = opt.et_get_loading_hours(duration=duration, remaining_time=remaining_time) + loading_hours = opt.ep_get_loading_hours(duration=duration, remaining_time=remaining_time) # evaluation assert loading_hours == expected_loading_hours @@ -256,18 +256,18 @@ def test_et_get_loading_hours(granularity, ) def test_et_charging_allowed(monkeypatch, provider_available, current_price, max_price, expected): opt = Optional() - monkeypatch.setattr(opt, "et_provider_available", Mock(return_value=provider_available)) + opt.data.electricity_pricing.configured = provider_available if provider_available: - monkeypatch.setattr(opt, "et_get_current_price", Mock(return_value=current_price)) - result = opt.et_is_charging_allowed_price_threshold(max_price) + monkeypatch.setattr(opt, "ep_get_current_price", Mock(return_value=current_price)) + result = opt.ep_is_charging_allowed_price_threshold(max_price) assert result == expected def test_et_charging_allowed_exception(monkeypatch): opt = Optional() - monkeypatch.setattr(opt, "et_provider_available", Mock(return_value=True)) - monkeypatch.setattr(opt, "et_get_current_price", Mock(side_effect=Exception)) - result = opt.et_is_charging_allowed_price_threshold(0.15) + opt.data.electricity_pricing.configured = True + monkeypatch.setattr(opt, "ep_get_current_price", Mock(side_effect=Exception)) + result = opt.ep_is_charging_allowed_price_threshold(0.15) assert result is False @@ -425,17 +425,18 @@ def test_et_charging_available(now_ts, provider_available, price_list, selected_ Mock(return_value=now_ts) ) opt = Optional() - opt.data.et.get.prices = price_list - monkeypatch.setattr(opt, "et_provider_available", Mock(return_value=provider_available)) - result = opt.et_is_charging_allowed_hours_list(selected_hours) + opt.data.electricity_pricing.get.prices = price_list + opt.data.electricity_pricing.configured = provider_available + result = opt.ep_is_charging_allowed_hours_list(selected_hours) assert result == expected def test_et_charging_available_exception(monkeypatch): opt = Optional() - monkeypatch.setattr(opt, "et_provider_available", Mock(return_value=True)) - opt.data.et.get.prices = {} # empty prices list raises exception - result = opt.et_is_charging_allowed_hours_list([]) + opt.data.electricity_pricing.configured = True + + opt.data.electricity_pricing.get.prices = {} # empty prices list raises exception + result = opt.ep_is_charging_allowed_hours_list([]) assert result is False @@ -446,10 +447,6 @@ def test_et_charging_available_exception(monkeypatch): {}, None, 1698224400, True, id="update_required_when_no_prices" ), - pytest.param( - {"1698224400": 0.1, "1698228000": 0.2}, None, 1698224400, False, - id="no_update_required_when_prices_available_and_recent" - ), pytest.param( {"1698224400": 0.1, "1698228000": 0.2}, 1698310800, 1698224400, False, id="no_update_required_when_next_query_time_not_reached" @@ -467,10 +464,11 @@ def test_et_charging_available_exception(monkeypatch): def test_et_price_update_required(monkeypatch, prices, next_query_time, current_timestamp, expected): # setup opt = Optional() - opt.data.et.get.prices = prices - opt.data.et.get.next_query_time = next_query_time + opt.data.electricity_pricing.get.prices = prices + opt.data.electricity_pricing.get.next_query_time = next_query_time monkeypatch.setattr(timecheck, "create_timestamp", Mock(return_value=current_timestamp)) + opt.data.electricity_pricing.configured = True # execution result = opt.et_price_update_required() diff --git a/packages/helpermodules/create_debug.py b/packages/helpermodules/create_debug.py index 1160b8290e..f5bb4b585c 100644 --- a/packages/helpermodules/create_debug.py +++ b/packages/helpermodules/create_debug.py @@ -504,7 +504,7 @@ def __on_connect_broker_essentials(self, client, userdata, flags, rc): client.subscribe("openWB/counter/#", 2) client.subscribe("openWB/pv/#", 2) client.subscribe("openWB/bat/#", 2) - client.subscribe("openWB/optional/et/provider", 2) + client.subscribe("openWB/optional/ep/flexible_tariff/provider", 2) def __on_connect_bridges(self, client, userdata, flags, rc): client.subscribe("openWB/system/mqtt/#", 2) diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index 36ccc2fc44..e80fd8a565 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -199,8 +199,9 @@ def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previou try: prices = data.data.general_data.data.prices try: - grid_price = data.data.optional_data.et_get_current_price() + grid_price = data.data.optional_data.ep_get_current_price() except Exception: + log.exception("Fehler im Werte-Logging-Modul für aktuellen Netzpreis, nutze hinterlegten Netzpreis") grid_price = prices.grid prices_dict = {"grid": grid_price, "pv": prices.pv, diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 18175fdd38..8ae3f3e980 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -847,16 +847,27 @@ def process_optional_topic(self, msg: mqtt.MQTTMessage): enthält Topic und Payload """ try: - if "openWB/set/optional/et/get/prices" in msg.topic: + pricing_regex = "openWB/set/optional/ep/(flexible_tariff|grid_fee)/" + if re.search(pricing_regex, msg.topic) is not None: + if re.search(f"{pricing_regex}provider$", msg.topic) is not None: + self._validate_value(msg, "json") + elif re.search(f"{pricing_regex}get/prices$", msg.topic) is not None: + self._validate_value(msg, "json") + elif re.search(f"{pricing_regex}get/price$", msg.topic) is not None: + self._validate_value(msg, float) + elif re.search(f"{pricing_regex}get/fault_state$", msg.topic) is not None: + self._validate_value(msg, int, [(0, 2)]) + elif re.search(f"{pricing_regex}get/fault_str$", msg.topic) is not None: + self._validate_value(msg, str) + elif "openWB/set/optional/ep/get/prices" in msg.topic: self._validate_value(msg, "json") - elif "openWB/set/optional/et/get/next_query_time" in msg.topic: + elif "openWB/set/optional/ep/get/next_query_time" in msg.topic: self._validate_value(msg, float) - elif "openWB/set/optional/et/get/fault_state" in msg.topic: - self._validate_value(msg, int, [(0, 2)]) - elif "openWB/set/optional/et/get/fault_str" in msg.topic: - self._validate_value(msg, str) - elif ("openWB/set/optional/et/provider" in msg.topic or - "openWB/set/optional/ocpp/config" in msg.topic): + elif "openWB/set/optional/ep/configured" in msg.topic: + self._validate_value(msg, bool) + elif "module_update_completed" in msg.topic: + self._validate_value(msg, bool) + elif "openWB/set/optional/ocpp/config" in msg.topic: self._validate_value(msg, "json") elif "openWB/set/optional/monitoring" in msg.topic: self._validate_value(msg, "json") diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index ae4e23bb87..1f43f8afe2 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -30,7 +30,7 @@ from dataclass_utils import dataclass_from_dict from modules.common.abstract_vehicle import CalculatedSocState, GeneralVehicleConfig from modules.common.configurable_backup_cloud import ConfigurableBackupCloud -from modules.common.configurable_tariff import ConfigurableElectricityTariff +from modules.common.configurable_tariff import ConfigurableFlexibleTariff, ConfigurableGridFee from modules.common.simcount.simcounter_state import SimCounterState from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, RfidData) @@ -712,23 +712,42 @@ def process_optional_topic(self, var: optional.Optional, msg: mqtt.MQTTMessage): run_command([ str(Path(__file__).resolve().parents[2] / "runs" / "update_local_display.sh") ], process_exception=True) - elif re.search("/optional/et/", msg.topic) is not None: - if re.search("/optional/et/get/prices", msg.topic) is not None: - var.data.et.get.prices = decode_payload(msg.payload) - elif re.search("/optional/et/get/", msg.topic) is not None: - self.set_json_payload_class(var.data.et.get, msg) - elif re.search("/optional/et/provider$", msg.topic) is not None: + elif re.search("/optional/ep/(flexible_tariff|grid_fee)/", msg.topic) is not None: + if re.search("/optional/ep/flexible_tariff/provider$", msg.topic) is not None: config_dict = decode_payload(msg.payload) if config_dict["type"] is None: - var.et_module = None + var.flexible_tariff_module = None else: mod = importlib.import_module( - f".electricity_tariffs.{config_dict['type']}.tariff", "modules") + f".electricity_pricing.flexible_tariffs.{config_dict['type']}.tariff", "modules") config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config_dict) - var.et_module = ConfigurableElectricityTariff(config, mod.create_electricity_tariff) - var.et_get_prices() - else: - self.set_json_payload_class(var.data.et, msg) + var.flexible_tariff_module = ConfigurableFlexibleTariff( + config, mod.create_electricity_tariff) + elif re.search("/optional/ep/flexible_tariff/get/prices", msg.topic) is not None: + var.data.electricity_pricing.flexible_tariff.get.prices = decode_payload(msg.payload) + elif re.search("/optional/ep/flexible_tariff/get/", msg.topic) is not None: + self.set_json_payload_class(var.data.electricity_pricing.flexible_tariff.get, msg) + elif re.search("/optional/ep/grid_fee/provider$", msg.topic) is not None: + config_dict = decode_payload(msg.payload) + if config_dict["type"] is None: + var.grid_fee_module = None + else: + mod = importlib.import_module( + f".electricity_pricing.grid_fees.{config_dict['type']}.tariff", "modules") + config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config_dict) + var.grid_fee_module = ConfigurableGridFee(config, mod.create_electricity_tariff) + elif re.search("/optional/ep/grid_fee/get/prices", msg.topic) is not None: + var.data.electricity_pricing.grid_fee.get.prices = decode_payload(msg.payload) + elif re.search("/optional/ep/grid_fee/get/", msg.topic) is not None: + self.set_json_payload_class(var.data.electricity_pricing.grid_fee.get, msg) + elif re.search("/optional/ep/get/prices", msg.topic) is not None: + var.data.electricity_pricing.get.prices = decode_payload(msg.payload) + elif re.search("/optional/ep/get/", msg.topic) is not None: + self.set_json_payload_class(var.data.electricity_pricing.get, msg) + elif re.search("/optional/ep/", msg.topic) is not None: + self.set_json_payload_class(var.data.electricity_pricing, msg) + elif "module_update_completed" in msg.topic: + self.event_module_update_completed.set() elif re.search("/optional/ocpp/", msg.topic) is not None: config_dict = decode_payload(msg.payload) var.data.ocpp = dataclass_from_dict(Ocpp, config_dict) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 749cfa51ed..d9da9a4148 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 101 + DATASTORE_VERSION = 102 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -314,10 +314,14 @@ class UpdateConfig: "^openWB/set/log/request", "^openWB/set/log/data", - "^openWB/optional/et/get/fault_state$", - "^openWB/optional/et/get/fault_str$", - "^openWB/optional/et/get/prices$", - "^openWB/optional/et/provider$", + "^openWB/optional/ep/flexible_tariff/get/fault_state$", + "^openWB/optional/ep/flexible_tariff/get/fault_str$", + "^openWB/optional/ep/flexible_tariff/get/prices$", + "^openWB/optional/ep/flexible_tariff/provider$", + "^openWB/optional/ep/grid_fee/get/fault_state$", + "^openWB/optional/ep/grid_fee/get/fault_str$", + "^openWB/optional/ep/grid_fee/get/prices$", + "^openWB/optional/ep/grid_fee/provider$", "^openWB/optional/int_display/active$", "^openWB/optional/int_display/detected$", "^openWB/optional/int_display/on_if_plugged_in$", @@ -475,7 +479,8 @@ class UpdateConfig: "^openWB/system/configurable/chargepoints$", "^openWB/system/configurable/chargepoints_internal$", "^openWB/system/configurable/devices_components$", - "^openWB/system/configurable/electricity_tariffs$", + "^openWB/system/configurable/flexible_tariffs$", + "^openWB/system/configurable/grid_fees$", "^openWB/system/configurable/display_themes$", "^openWB/system/configurable/io_actions$", "^openWB/system/configurable/io_devices$", @@ -574,7 +579,8 @@ class UpdateConfig: ("openWB/graph/config/duration", 120), ("openWB/internal_chargepoint/0/data/parent_cp", None), ("openWB/internal_chargepoint/1/data/parent_cp", None), - ("openWB/optional/et/provider", NO_MODULE), + ("openWB/optional/ep/flexible_tariff/provider", NO_MODULE), + ("openWB/optional/ep/grid_fee/provider", NO_MODULE), ("openWB/optional/int_display/active", True), ("openWB/optional/int_display/detected", True), ("openWB/optional/int_display/on_if_plugged_in", True), @@ -2103,7 +2109,7 @@ def upgrade(topic: str, payload) -> Optional[dict]: configuration_payload.update({"official": True}) return {topic: configuration_payload} # add "official" flag to selected electricity tariff provider - if re.search("openWB/optional/et/provider", topic) is not None: + if re.search("openWB/optional/ep/flexible_tariff/provider", topic) is not None: configuration_payload = decode_payload(payload) official_providers = ["awattar", "energycharts", "rabot", "tibber", "voltego"] if configuration_payload.get("type") in official_providers: @@ -2625,3 +2631,10 @@ def upgrade(topic: str, payload) -> None: Pub().pub(topic, payload) self._loop_all_received_topics(upgrade) self._append_datastore_version(101) + + def upgrade_datastore_102(self) -> None: + def upgrade(topic: str, payload) -> None: + if "openWB/optional/et/provider" == topic: + return {"openWB/optional/ep/flexible_tariff/provider": decode_payload(payload)} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 102) diff --git a/packages/main.py b/packages/main.py index 88486b9287..ab9aeba482 100755 --- a/packages/main.py +++ b/packages/main.py @@ -151,6 +151,7 @@ def handler_with_control_interval(): control.calc_current() proc.process_algorithm_results() data.data.graph_data.pub_graph_data() + loadvars_.ep_get_prices() self.interval_counter = 1 else: self.interval_counter = self.interval_counter + 1 @@ -187,7 +188,7 @@ def handler5MinAlgorithm(self): data.data.general_data.grid_protection() data.data.optional_data.ocpp_transfer_meter_values() data.data.counter_all_data.validate_hierarchy() - data.data.optional_data.et_get_prices() + loadvars_.ep_get_prices() except Exception: log.exception("Fehler im Main-Modul") diff --git a/packages/modules/common/component_type.py b/packages/modules/common/component_type.py index d7a3b4b4bc..a759d3c533 100644 --- a/packages/modules/common/component_type.py +++ b/packages/modules/common/component_type.py @@ -7,7 +7,8 @@ class ComponentType(Enum): BAT = "bat" CHARGEPOINT = "cp" COUNTER = "counter" - ELECTRICITY_TARIFF = "electricity_tariff" + FLEXIBLE_TARIFF = "dynamic_tariff" + GRID_FEE = "grid_tariff" INVERTER = "inverter" IO = "io" @@ -32,8 +33,10 @@ def type_to_topic_mapping(component_type: str) -> str: return "counter" elif "inverter" in component_type: return "pv" - elif ComponentType.ELECTRICITY_TARIFF.value in component_type: - return "optional/et" + elif ComponentType.FLEXIBLE_TARIFF.value in component_type: + return "optional/ep/flexible_tariff" + elif ComponentType.GRID_FEE.value in component_type: + return "optional/ep/grid_fee" elif ComponentType.IO.value in component_type: return "io/states" else: diff --git a/packages/modules/common/configurable_tariff.py b/packages/modules/common/configurable_tariff.py index b83b087d88..a525bb52e7 100644 --- a/packages/modules/common/configurable_tariff.py +++ b/packages/modules/common/configurable_tariff.py @@ -14,13 +14,12 @@ log = logging.getLogger(__name__) -class ConfigurableElectricityTariff(Generic[T_TARIFF_CONFIG]): +class ConfigurableTariff(Generic[T_TARIFF_CONFIG]): def __init__(self, config: T_TARIFF_CONFIG, component_initializer: Callable[[], float]) -> None: self.config = config - self.store = store.get_electricity_tariff_value_store() - self.fault_state = FaultState(ComponentInfo(None, self.config.name, ComponentType.ELECTRICITY_TARIFF.value)) + # nach Init auf NO_ERROR setzen, damit der Fehlerstatus beim Modulwechsel gelöscht wird self.fault_state.no_error() self.fault_state.store_error() @@ -76,3 +75,21 @@ def _remove_outdated_prices(self, tariff_state: TariffState, timeslot_length_sec 'Die Preisliste startet nicht mit der aktuellen Stunde. ' f'Eintrag {timestamp} wurden entfernt. rest: {tariff_state.prices}') return tariff_state + + +class ConfigurableFlexibleTariff(ConfigurableTariff): + def __init__(self, + config: T_TARIFF_CONFIG, + component_initializer: Callable[[], float]) -> None: + self.store = store.get_flexible_tariff_value_store() + self.fault_state = FaultState(ComponentInfo(None, config.name, ComponentType.FLEXIBLE_TARIFF.value)) + super().__init__(config, component_initializer) + + +class ConfigurableGridFee(ConfigurableTariff): + def __init__(self, + config: T_TARIFF_CONFIG, + component_initializer: Callable[[], float]) -> None: + self.store = store.get_grid_fee_value_store() + self.fault_state = FaultState(ComponentInfo(None, config.name, ComponentType.GRID_FEE.value)) + super().__init__(config, component_initializer) diff --git a/packages/modules/common/configurable_tariff_test.py b/packages/modules/common/configurable_tariff_test.py index d4b8e4bc1e..b5f274e162 100644 --- a/packages/modules/common/configurable_tariff_test.py +++ b/packages/modules/common/configurable_tariff_test.py @@ -3,8 +3,8 @@ import pytest from modules.common.component_state import TariffState -from modules.common.configurable_tariff import ConfigurableElectricityTariff -from modules.electricity_tariffs.awattar.config import AwattarTariff +from modules.common.configurable_tariff import ConfigurableFlexibleTariff +from modules.electricity_pricing.flexible_tariffs.awattar.config import AwattarTariff @pytest.mark.parametrize( @@ -87,7 +87,7 @@ def test_remove_outdated_prices( now: int, tariff_state: TariffState, expected: TariffState, monkeypatch ): # setup - tariff = ConfigurableElectricityTariff(AwattarTariff(), Mock()) + tariff = ConfigurableFlexibleTariff(AwattarTariff(), Mock()) time_slot_seconds = [int(timestamp) for timestamp in tariff_state.prices.keys()][:2] # Montag 16.05.2022, 8:40:52 "05/16/2022, 08:40:52" Unix: 1652683252 @@ -104,7 +104,7 @@ def test_remove_outdated_prices( def test_accept_no_prices_at_start(monkeypatch): # setup - tariff = ConfigurableElectricityTariff( + tariff = ConfigurableFlexibleTariff( AwattarTariff(), Mock( return_value=TariffState( diff --git a/packages/modules/common/fault_state.py b/packages/modules/common/fault_state.py index 7c4756174a..ec944c1385 100644 --- a/packages/modules/common/fault_state.py +++ b/packages/modules/common/fault_state.py @@ -47,7 +47,8 @@ def store_error(self) -> None: self.fault_str + ", Traceback: \n" + traceback.format_exc()) topic = component_type.type_to_topic_mapping(self.component_info.type) - if self.component_info.type == component_type.ComponentType.ELECTRICITY_TARIFF.value: + if (self.component_info.type == component_type.ComponentType.FLEXIBLE_TARIFF.value or + self.component_info.type == component_type.ComponentType.GRID_FEE.value): topic_prefix = f"openWB/set/{topic}" else: topic_prefix = f"openWB/set/{topic}/{self.component_info.id}" diff --git a/packages/modules/common/store/__init__.py b/packages/modules/common/store/__init__.py index 451f2ce2c1..4c76f0c1fd 100644 --- a/packages/modules/common/store/__init__.py +++ b/packages/modules/common/store/__init__.py @@ -6,6 +6,6 @@ from modules.common.store._counter import get_counter_value_store from modules.common.store._inverter import get_inverter_value_store from modules.common.store._io import get_io_value_store -from modules.common.store._tariff import get_electricity_tariff_value_store +from modules.common.store._tariff import get_flexible_tariff_value_store, get_grid_fee_value_store from modules.common.store.ramdisk.io import ramdisk_write, ramdisk_read, ramdisk_read_float, ramdisk_read_int, \ RAMDISK_PATH diff --git a/packages/modules/common/store/_tariff.py b/packages/modules/common/store/_tariff.py index 8a899be7fb..0dedce1328 100644 --- a/packages/modules/common/store/_tariff.py +++ b/packages/modules/common/store/_tariff.py @@ -1,3 +1,5 @@ +from datetime import timedelta +from control import data from modules.common.component_state import TariffState from modules.common.fault_state import FaultState from modules.common.store import ValueStore @@ -9,7 +11,7 @@ log = logging.getLogger(__name__) -class TariffValueStoreBroker(ValueStore[TariffState]): +class FlexibleTariffValueStore(ValueStore[TariffState]): def __init__(self): pass @@ -19,11 +21,99 @@ def set(self, state: TariffState) -> None: def update(self): try: prices = self.state.prices - pub_to_broker("openWB/set/optional/et/get/prices", prices) + pub_to_broker("openWB/set/optional/ep/flexible_tariff/get/prices", prices) log.debug(f"published prices list to MQTT having {len(prices)} entries") except Exception as e: raise FaultState.from_exception(e) -def get_electricity_tariff_value_store() -> ValueStore[TariffState]: - return LoggingValueStore(TariffValueStoreBroker()) +def get_flexible_tariff_value_store() -> ValueStore[TariffState]: + return LoggingValueStore(FlexibleTariffValueStore()) + + +class GridFeeValueStore(ValueStore[TariffState]): + def __init__(self): + pass + + def set(self, state: TariffState) -> None: + self.state = state + + def update(self): + try: + prices = self.state.prices + pub_to_broker("openWB/set/optional/ep/grid_fee/get/prices", prices) + log.debug(f"published grid tariff prices list to MQTT having {len(prices)} entries") + except Exception as e: + raise FaultState.from_exception(e) + + +def get_grid_fee_value_store() -> ValueStore[TariffState]: + return LoggingValueStore(GridFeeValueStore()) + + +class PriceValueStore(ValueStore[TariffState]): + def __init__(self): + pass + + def update(self): + try: + pub_to_broker("openWB/set/optional/ep/get/prices", self.sum_prices()) + except Exception as e: + raise FaultState.from_exception(e) + + def sum_prices(self): + flexible_tariff_prices = data.data.optional_data.data.electricity_pricing.flexible_tariff.get.prices + if len(flexible_tariff_prices) == 0 and data.data.optional_data.flexible_tariff_module is not None: + raise ValueError("Keine Preise für konfigurierten dynamischen Stromtarif vorhanden.") + grid_fee_prices = data.data.optional_data.data.electricity_pricing.grid_fee.get.prices + if len(grid_fee_prices) == 0 and data.data.optional_data.grid_fee_module is not None: + raise ValueError("Keine Preise für konfigurierten Netzentgelttarif vorhanden.") + flexible_tariff_prices = {float(k): v for k, v in flexible_tariff_prices.items()} + grid_fee_prices = {float(k): v for k, v in grid_fee_prices.items()} + if len(flexible_tariff_prices) == 0 and len(grid_fee_prices) > 0: + return grid_fee_prices + if len(grid_fee_prices) == 0 and len(flexible_tariff_prices) > 0: + return flexible_tariff_prices + + grid_fee_keys = sorted(grid_fee_prices.keys()) + flexible_tariff_keys = sorted(flexible_tariff_prices.keys()) + + def median_delta(keys): + """Typische Schrittweite bestimmen (Median der Deltas)""" + if len(keys) < 2: + return timedelta.max + deltas = [(keys[i+1] - keys[i]) for i in range(len(keys)-1)] + deltas.sort() + return timedelta(seconds=deltas[len(deltas)//2]) + grid_fee_delta = median_delta(grid_fee_keys) + electricity_tariff_delta = median_delta(flexible_tariff_keys) + # Feinere und gröbere Auflösung bestimmen + if grid_fee_delta < electricity_tariff_delta: + fine_dict, coarse_dict = grid_fee_prices, flexible_tariff_prices + else: + fine_dict, coarse_dict = flexible_tariff_prices, grid_fee_prices + # Intervallgrenzen für das gröbere Dict + coarse_keys = sorted(coarse_dict.keys()) + intervalle = [] + for i, start in enumerate(coarse_keys): + if i+1 < len(coarse_keys): + ende = coarse_keys[i+1] + else: + ende = max(fine_dict.keys()) + 1 + intervalle.append((start, ende)) + # Für jeden feinen Zeitstempel das passende grobe Intervall suchen und addieren + result = {} + for ts_fine, preis_fine in fine_dict.items(): + coarse_value = None + for start, ende in intervalle: + if start <= ts_fine < ende: + coarse_value = coarse_dict[start] + break + if coarse_value is None: + raise ValueError(f"Kein passendes Intervall für {ts_fine}") + result[ts_fine] = preis_fine + coarse_value + return result + + +def get_price_value_store() -> ValueStore[TariffState]: + return PriceValueStore() diff --git a/packages/modules/common/store/_tariff_test.py b/packages/modules/common/store/_tariff_test.py new file mode 100644 index 0000000000..d880a9bccc --- /dev/null +++ b/packages/modules/common/store/_tariff_test.py @@ -0,0 +1,66 @@ +from datetime import timedelta +import datetime +from typing import Dict +from unittest.mock import Mock + +import pytest + +from control import data +from control.optional import Optional +from modules.common.store._tariff import PriceValueStore + + +def make_prices(count, step, base): + start = datetime.datetime.fromtimestamp(1761127200.0) # 2025-10-22 12:00 + # json.loads macht keys immer zu str + return {str((start + timedelta(minutes=step*i)).timestamp()): base+i for i in range(count)} + + +@pytest.fixture(autouse=True) +def mock_data() -> None: + data.data_init(Mock()) + data.data.optional_data = Optional() + + +@pytest.mark.parametrize("flexible_tariff, grid_fee, expected_prices", [ + pytest.param(make_prices(4, 15, 10), make_prices(12, 5, 1), {1761127200: 11, + 1761127500: 12, + 1761127800: 13, + 1761128100: 15, + 1761128400: 16, + 1761128700: 17, + 1761129000: 19, + 1761129300: 20, + 1761129600: 21, + 1761129900: 23, + 1761130200: 24, + 1761130500: 25}, + id="grid_fee_finer"), # grid_fee: 12x5min, flexible_tariff: 4x15min + pytest.param(make_prices(12, 5, 1), make_prices(4, 14, 10), {1761127200: 11, + 1761127500: 12, + 1761127800: 13, + 1761128100: 15, + 1761128400: 16, + 1761128700: 17, + 1761129000: 19, + 1761129300: 20, + 1761129600: 21, + 1761129900: 23, + 1761130200: 24, + 1761130500: 25}, + id="flexible tariff finer"), # flexible_tariff: 12x5min, grid_fee: 4x14min + pytest.param(make_prices(4, 15, 1), make_prices(4, 15, 10), {1761127200: 11, + 1761128100: 13, + 1761129000: 15, + 1761129900: 17}, + id="same resolution"), # flexible_tariff & grid_fee: 4x15min +]) +def test_sum_prices(flexible_tariff: Dict[int, float], + grid_fee: Dict[int, float], + expected_prices: Dict[int, float]): + value_Store = PriceValueStore() + data.data.optional_data.data.electricity_pricing.flexible_tariff.get.prices = flexible_tariff + data.data.optional_data.data.electricity_pricing.grid_fee.get.prices = grid_fee + summed = value_Store.sum_prices() + for timestamp, price in summed.items(): + assert price == expected_prices[timestamp] diff --git a/packages/modules/configuration.py b/packages/modules/configuration.py index ba1aa23150..b9ab393811 100644 --- a/packages/modules/configuration.py +++ b/packages/modules/configuration.py @@ -15,7 +15,7 @@ def pub_configurable(): _pub_configurable_backup_clouds() _pub_configurable_web_themes() _pub_configurable_display_themes() - _pub_configurable_electricity_tariffs() + _pub_configurable_tariffs() _pub_configurable_soc_modules() _pub_configurable_devices_components() _pub_configurable_chargepoints() @@ -109,40 +109,44 @@ def _pub_configurable_display_themes() -> None: log.exception("Fehler im configuration-Modul") -def _pub_configurable_electricity_tariffs() -> None: - try: - electricity_tariffs: List[Dict] = [] - path_list = Path(_get_packages_path()/"modules"/"electricity_tariffs").glob('**/tariff.py') - for path in path_list: - try: - if path.name.endswith("_test.py"): - # Tests überspringen - continue - dev_defaults = importlib.import_module( - f".electricity_tariffs.{path.parts[-2]}.tariff", - "modules").device_descriptor.configuration_factory() - electricity_tariffs.append({ - "value": dev_defaults.type, - "text": dev_defaults.name, - "defaults": dataclass_utils.asdict(dev_defaults) - }) - except Exception: - log.exception("Fehler im configuration-Modul") - electricity_tariffs = sorted(electricity_tariffs, key=lambda d: d['text'].upper()) - # "leeren" Eintrag an erster Stelle einfügen - electricity_tariffs.insert(0, - { - "value": None, - "text": "- kein Anbieter -", - "defaults": { - "type": None, - "configuration": {} - } - }) - - Pub().pub("openWB/set/system/configurable/electricity_tariffs", electricity_tariffs) - except Exception: - log.exception("Fehler im configuration-Modul") +def _pub_configurable_tariffs() -> None: + def pub(source: str): + try: + tariffs: List[Dict] = [] + path_list = Path(_get_packages_path()/"modules"/"electricity_pricing" / + f"{source}").glob('**/tariff.py') + for path in path_list: + try: + if path.name.endswith("_test.py"): + # Tests überspringen + continue + dev_defaults = importlib.import_module( + f".electricity_pricing.{source}.{path.parts[-2]}.tariff", + "modules").device_descriptor.configuration_factory() + tariffs.append({ + "value": dev_defaults.type, + "text": dev_defaults.name, + "defaults": dataclass_utils.asdict(dev_defaults) + }) + except Exception: + log.exception("Fehler im configuration-Modul") + tariffs = sorted(tariffs, key=lambda d: d['text'].upper()) + # "leeren" Eintrag an erster Stelle einfügen + tariffs.insert(0, + { + "value": None, + "text": "- kein Anbieter -", + "defaults": { + "type": None, + "configuration": {} + } + }) + + Pub().pub(f"openWB/set/system/configurable/{source}", tariffs) + except Exception: + log.exception("Fehler im configuration-Modul") + pub("flexible_tariffs") + pub("grid_fees") def _pub_configurable_soc_modules() -> None: diff --git a/packages/modules/display_themes/cards/source/src/App.vue b/packages/modules/display_themes/cards/source/src/App.vue index 1433979f83..104594f8f9 100644 --- a/packages/modules/display_themes/cards/source/src/App.vue +++ b/packages/modules/display_themes/cards/source/src/App.vue @@ -55,8 +55,8 @@ export default { "openWB/counter/get/hierarchy", "openWB/counter/set/home_consumption", "openWB/general/chargemode_config/pv_charging/bat_mode", - "openWB/optional/et/provider", - "openWB/optional/et/get/prices", + "openWB/optional/ep/configured", + "openWB/optional/ep/get/prices", "openWB/optional/int_display/theme", "openWB/optional/int_display/standby", "openWB/optional/rfid/active", diff --git a/packages/modules/display_themes/cards/source/src/stores/mqtt.js b/packages/modules/display_themes/cards/source/src/stores/mqtt.js index a58913e70e..91d0e89cb7 100644 --- a/packages/modules/display_themes/cards/source/src/stores/mqtt.js +++ b/packages/modules/display_themes/cards/source/src/stores/mqtt.js @@ -947,16 +947,15 @@ export const useMqttStore = defineStore("mqtt", { /* electricity tariff provider */ getEtConfigured(state){ if ( - state.topics["openWB/optional/et/provider"] !== + state.topics["openWB/optional/ep/configured"] !== undefined ) { - return state.topics["openWB/optional/et/provider"] - .type !== null; + return state.topics["openWB/optional/ep/configured"]; } return false; }, getEtPrices(state) { - return state.topics["openWB/optional/et/get/prices"]; + return state.topics["openWB/optional/ep/get/prices"]; }, }, actions: { diff --git a/packages/modules/display_themes/colors/source/src/components/priceChart/PriceChart.vue b/packages/modules/display_themes/colors/source/src/components/priceChart/PriceChart.vue index 62e29ef3dd..60e5d9bbc8 100755 --- a/packages/modules/display_themes/colors/source/src/components/priceChart/PriceChart.vue +++ b/packages/modules/display_themes/colors/source/src/components/priceChart/PriceChart.vue @@ -1,7 +1,6 @@ -
{{ etData.etProvider }}
diff --git a/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue b/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue index b8fe8d7491..89882f9dbd 100755 --- a/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue +++ b/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue @@ -1,5 +1,4 @@

Anbieter: {{ etData.etProvider }}

diff --git a/packages/modules/web_themes/colors/source/src/components/priceChart/processMessages.ts b/packages/modules/web_themes/colors/source/src/components/priceChart/processMessages.ts index b17d52c803..5f2d81565d 100755 --- a/packages/modules/web_themes/colors/source/src/components/priceChart/processMessages.ts +++ b/packages/modules/web_themes/colors/source/src/components/priceChart/processMessages.ts @@ -2,18 +2,17 @@ import { globalData } from '@/assets/js/model' import { etData } from './model' export function processEtProviderMessages(topic: string, message: string) { - if (topic == 'openWB/optional/et/provider') { + if (topic == 'openWB/optional/ep/configured') { const data = JSON.parse(message) if (data.type == null) { etData.active = false } else { etData.active = true - etData.etProvider = JSON.parse(message).name } if (data.configuration && data.configuration.country != null) { globalData.country = data.configuration.country } - } else if (topic == 'openWB/optional/et/get/prices') { + } else if (topic == 'openWB/optional/ep/get/prices') { const plist = JSON.parse(message) etData.etPriceList = new Map() Object.keys(plist).forEach((datestring) => { diff --git a/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts b/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts index 9eeb4c4b68..25b4a837a4 100644 --- a/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts +++ b/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts @@ -3523,16 +3523,16 @@ export const useMqttStore = defineStore('mqtt', () => { /* electricity tariff provider */ const etProviderConfigured = computed(() => { return ( - ((getValue.value( - 'openWB/optional/et/provider', - 'type', - null, - ) as string) || undefined) !== undefined + (getValue.value( + 'openWB/optional/ep/configured', + undefined, + false, + ) as boolean) || false ); }); const etPrices = computed(() => { - return getValue.value('openWB/optional/et/get/prices', undefined, {}) as { + return getValue.value('openWB/optional/ep/get/prices', undefined, {}) as { [key: string]: number; }; }); diff --git a/packages/modules/web_themes/koala/web/index.html b/packages/modules/web_themes/koala/web/index.html index e143bef156..5cce1c53b8 100644 --- a/packages/modules/web_themes/koala/web/index.html +++ b/packages/modules/web_themes/koala/web/index.html @@ -1,3 +1,3 @@ -openWB +openWB
\ No newline at end of file diff --git a/packages/modules/web_themes/koala/web/sw.js b/packages/modules/web_themes/koala/web/sw.js index 79481ac29d..68352f6b44 100644 --- a/packages/modules/web_themes/koala/web/sw.js +++ b/packages/modules/web_themes/koala/web/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const i=(i,c)=>(i=new URL(i+".js",c).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(c,a)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(s[n])return;let o={};const r=e=>i(e,n),f={module:{uri:n},exports:o,require:r};s[n]=Promise.all(c.map(e=>f[e]||r(e))).then(e=>(a(...e),o))}}define(["./workbox-e8110d74"],function(e){"use strict";e.setCacheNameDetails({prefix:"openwb-koala-web-theme"}),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/_plugin-vue_export-helper-DeZhcsWf.js",revision:"351c52e0196de4664830fd671205bd0e"},{url:"assets/ErrorNotFound-DTsZXF-9.js",revision:"15a3fff156be2e70d6a573ad6b95d0ad"},{url:"assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff",revision:"3e1afe59fa075c9e04c436606b77f640"},{url:"assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2",revision:"a4160421d2605545f69a4cd6cd642902"},{url:"assets/index-BDCPzJkG.js",revision:"efd464a638cba9fe9e870ccced10ea59"},{url:"assets/index-CQyGg22l.css",revision:"bc877c1c6592e0ba74c68556fd9ddba4"},{url:"assets/IndexPage-DZBTYnl4.css",revision:"42e6edb4cc33745c8afce4748639a19c"},{url:"assets/IndexPage-pRRYctR0.js",revision:"150b1edd997ae34ae637f47a0bd40d1c"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff",revision:"2d29775851b8463053deb35b21b5d5c8"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff",revision:"be27354f07345fafe8dfc84117bbafd4"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff",revision:"c8cea161abfb039c97a11c26dff2f546"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff",revision:"585ad11be98f8f044923a71898ddfde6"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff",revision:"2cadc82e8484ccac69caddc849f603be"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff",revision:"51c41b1c2668c088c7cce3fa116396e1"},{url:"assets/MainLayout-BAVK94gH.js",revision:"367faa9d2c2e11098e0b83300054e69e"},{url:"assets/MainLayout-DZ5KVho1.css",revision:"704025cf47332d32f6495b6dff143158"},{url:"assets/mqtt-store-Cy2Yk4RJ.js",revision:"52aa53002287ba5f45aab6183d2c5940"},{url:"assets/store-init-Dir59g6z.js",revision:"60a7ef550d33b2a52cfa1d6ddbe5228c"},{url:"favicon.ico",revision:"21ec9d90ddf9217f71db985765970460"},{url:"icons/favicon-128x128.png",revision:"11a65eacbba8a7148cb1d00f77d5307a"},{url:"icons/favicon-16x16.png",revision:"7834f799f3bc7a6adb0704cb8a25a2c6"},{url:"icons/favicon-32x32.png",revision:"0d30a992b5f03bd43f1b393917dc1714"},{url:"icons/favicon-96x96.png",revision:"830d36dc970ffa9592e61af7ef771d5d"},{url:"icons/icon_Data.md",revision:"894ff587e880d3d4a38fbac654fe3e87"},{url:"icons/icon-1024x1024.png",revision:"e261ff64a764a7c5802309561cf190d4"},{url:"icons/icon-128x128.png",revision:"86dad79e449e75c39c6d373b85b9f486"},{url:"icons/icon-192x192.png",revision:"bc936b6464631f18b89afd54ed62d04d"},{url:"icons/icon-256x256.png",revision:"9043ec80cc89c1c6610b209325f94360"},{url:"icons/icon-48x48.png",revision:"ba12339bebba853069fdcad3ae2a6e40"},{url:"icons/icon-512x512.png",revision:"ed6ff495b6d0d2240682714e78d1a9f2"},{url:"icons/icon-72x72.png",revision:"a4fe9ae16f8df3b31b1392a9e1e17aa3"},{url:"icons/icon-96x96.png",revision:"2f9771755fc8559353a62023c3b31976"},{url:"icons/openWB_logo_dark.png",revision:"fb8fe2728744be30d7beb07a78c6d886"},{url:"icons/owbBattery.svg",revision:"2dab2270a8d6635973fdf76a97593251"},{url:"icons/owbChargePoint.svg",revision:"90c176e44597f381ecd42a98eaf2a2c5"},{url:"icons/owbGrid.svg",revision:"526801681d42a6a4e7657b79d90cab17"},{url:"icons/owbHouse.svg",revision:"59e1d89587242168650bb57a8ac4ce6c"},{url:"icons/owbPV.svg",revision:"f4c69e6878b18cbc53fd03df0e211197"},{url:"icons/owbVehicle.svg",revision:"1107cfeed5240caa0838ee8493f02e34"},{url:"index.html",revision:"c5ae30cef814ca026a17ac5fef30246a"},{url:"manifest.json",revision:"291b1cf54805e7cc540854a2601f651b"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"),{denylist:[/sw\.js$/,/workbox-(.)*\\.js$/]}))}); +if(!self.define){let e,s={};const i=(i,c)=>(i=new URL(i+".js",c).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(c,n)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let a={};const f=e=>i(e,o),r={module:{uri:o},exports:a,require:f};s[o]=Promise.all(c.map(e=>r[e]||f(e))).then(e=>(n(...e),a))}}define(["./workbox-e8110d74"],function(e){"use strict";e.setCacheNameDetails({prefix:"openwb-koala-web-theme"}),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/_plugin-vue_export-helper-Bl94VFnD.js",revision:"32449ebf21b895cef6a8d1d65c02c0f1"},{url:"assets/ErrorNotFound-DPUVPZkx.js",revision:"8b501182e82fc45e6362568093fa1921"},{url:"assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff",revision:"3e1afe59fa075c9e04c436606b77f640"},{url:"assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2",revision:"a4160421d2605545f69a4cd6cd642902"},{url:"assets/index-CDG3Y43z.js",revision:"72955b0b7bc40a612052b6e5290361ad"},{url:"assets/index-CQyGg22l.css",revision:"bc877c1c6592e0ba74c68556fd9ddba4"},{url:"assets/IndexPage-DZBTYnl4.css",revision:"42e6edb4cc33745c8afce4748639a19c"},{url:"assets/IndexPage-NAs1b12H.js",revision:"ff5cffe12c6998600b5230a270f5170f"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff",revision:"2d29775851b8463053deb35b21b5d5c8"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff",revision:"be27354f07345fafe8dfc84117bbafd4"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff",revision:"c8cea161abfb039c97a11c26dff2f546"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff",revision:"585ad11be98f8f044923a71898ddfde6"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff",revision:"2cadc82e8484ccac69caddc849f603be"},{url:"assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff",revision:"51c41b1c2668c088c7cce3fa116396e1"},{url:"assets/MainLayout-Do3R5UFi.js",revision:"615b9542e84148ced31773ce1539bd4f"},{url:"assets/MainLayout-DZ5KVho1.css",revision:"704025cf47332d32f6495b6dff143158"},{url:"assets/mqtt-store-ByWA9yXf.js",revision:"79692c160827b19c42a99773c57aecba"},{url:"assets/store-init-UHG2X0tX.js",revision:"8af18fa4a7cfbc647a150b7db2b3ddb5"},{url:"favicon.ico",revision:"21ec9d90ddf9217f71db985765970460"},{url:"icons/favicon-128x128.png",revision:"11a65eacbba8a7148cb1d00f77d5307a"},{url:"icons/favicon-16x16.png",revision:"7834f799f3bc7a6adb0704cb8a25a2c6"},{url:"icons/favicon-32x32.png",revision:"0d30a992b5f03bd43f1b393917dc1714"},{url:"icons/favicon-96x96.png",revision:"830d36dc970ffa9592e61af7ef771d5d"},{url:"icons/icon_Data.md",revision:"894ff587e880d3d4a38fbac654fe3e87"},{url:"icons/icon-1024x1024.png",revision:"e261ff64a764a7c5802309561cf190d4"},{url:"icons/icon-128x128.png",revision:"86dad79e449e75c39c6d373b85b9f486"},{url:"icons/icon-192x192.png",revision:"bc936b6464631f18b89afd54ed62d04d"},{url:"icons/icon-256x256.png",revision:"9043ec80cc89c1c6610b209325f94360"},{url:"icons/icon-48x48.png",revision:"ba12339bebba853069fdcad3ae2a6e40"},{url:"icons/icon-512x512.png",revision:"ed6ff495b6d0d2240682714e78d1a9f2"},{url:"icons/icon-72x72.png",revision:"a4fe9ae16f8df3b31b1392a9e1e17aa3"},{url:"icons/icon-96x96.png",revision:"2f9771755fc8559353a62023c3b31976"},{url:"icons/openWB_logo_dark.png",revision:"fb8fe2728744be30d7beb07a78c6d886"},{url:"icons/owbBattery.svg",revision:"2dab2270a8d6635973fdf76a97593251"},{url:"icons/owbChargePoint.svg",revision:"90c176e44597f381ecd42a98eaf2a2c5"},{url:"icons/owbGrid.svg",revision:"526801681d42a6a4e7657b79d90cab17"},{url:"icons/owbHouse.svg",revision:"59e1d89587242168650bb57a8ac4ce6c"},{url:"icons/owbPV.svg",revision:"f4c69e6878b18cbc53fd03df0e211197"},{url:"icons/owbVehicle.svg",revision:"1107cfeed5240caa0838ee8493f02e34"},{url:"index.html",revision:"25cff5762d6c7d70225b23213c43d6fd"},{url:"manifest.json",revision:"291b1cf54805e7cc540854a2601f651b"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"),{denylist:[/sw\.js$/,/workbox-(.)*\\.js$/]}))}); diff --git a/packages/modules/web_themes/standard_legacy/web/index.html b/packages/modules/web_themes/standard_legacy/web/index.html index 31d0d357a2..4d31ef1b29 100644 --- a/packages/modules/web_themes/standard_legacy/web/index.html +++ b/packages/modules/web_themes/standard_legacy/web/index.html @@ -246,14 +246,6 @@ Strompreis:
-
-
- Anbieter -
-
- -- -
-
aktuell diff --git a/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js b/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js index 5c61619098..3c7a639d7d 100644 --- a/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js +++ b/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js @@ -1170,15 +1170,14 @@ function processGraphMessages(mqttTopic, mqttPayload) { function processETProviderMessages(mqttTopic, mqttPayload) { // processes mqttTopic for topic openWB/optional/et - if (mqttTopic == 'openWB/optional/et/provider') { - data = JSON.parse(mqttPayload); - if (data.type) { - $('.et-name').text(data.name); + if (mqttTopic == 'openWB/optional/ep/configured') { + const configured = JSON.parse(mqttPayload); + if (configured) { $('.et-configured').removeClass('hide'); } else { $('.et-configured').addClass('hide'); } - } else if (mqttTopic == 'openWB/optional/et/get/prices') { + } else if (mqttTopic == 'openWB/optional/ep/get/prices') { electricityPriceList = JSON.parse(mqttPayload); var currentPrice = electricityPriceList[Object.keys(electricityPriceList)[0]] * 100000; $('.et-current-price').text(currentPrice.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }) + ' ct/kWh'); diff --git a/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js b/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js index 335c619288..94dbee59e7 100644 --- a/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js +++ b/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js @@ -77,8 +77,8 @@ var topicsToSubscribe = [ // electricity tariff ["openWB/optional/et/active", 1], // et provider is configured - ["openWB/optional/et/provider", 1], // et provider information - ["openWB/optional/et/get/prices", 1], // current price list + ["openWB/optional/ep/configured", 1], // et provider information + ["openWB/optional/ep/get/prices", 1], // current price list ["openWB/optional/dc_charging", 1], // dc charging is configured // graph topics From 5d0fb96a1a2acbac31b86a0b30fc89ea90d42cd8 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 14 Nov 2025 09:28:13 +0100 Subject: [PATCH 2/2] fix pytest --- packages/control/optional_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/control/optional_test.py b/packages/control/optional_test.py index b1d9d516cc..c219868af8 100644 --- a/packages/control/optional_test.py +++ b/packages/control/optional_test.py @@ -464,6 +464,8 @@ def test_et_charging_available_exception(monkeypatch): def test_et_price_update_required(monkeypatch, prices, next_query_time, current_timestamp, expected): # setup opt = Optional() + opt._flexible_tariff_module = Mock() + opt._grid_fee_module = Mock() opt.data.electricity_pricing.get.prices = prices opt.data.electricity_pricing.get.next_query_time = next_query_time