From d0d21f5c9134e6d93f88d3a9e4def1fde30c51b9 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 15 Jun 2026 10:58:23 +0200 Subject: [PATCH 1/3] increase robustness of dataclass from dict; add missing electricity pricing update config --- .../dataclass_utils/_dataclass_from_dict.py | 37 ++++++++++++++----- packages/helpermodules/update_config.py | 26 ++++++++++++- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_from_dict.py b/packages/dataclass_utils/_dataclass_from_dict.py index e9f73e9f12..4e626d8a5c 100644 --- a/packages/dataclass_utils/_dataclass_from_dict.py +++ b/packages/dataclass_utils/_dataclass_from_dict.py @@ -1,4 +1,5 @@ from enum import Enum +from dataclasses import MISSING, fields, is_dataclass import inspect from inspect import FullArgSpec, isclass from typing import TypeVar, Type, Union, get_args, get_origin @@ -26,22 +27,38 @@ def dataclass_from_dict(cls: Type[T], args: Union[dict, T]) -> T: elif isinstance(args, type(cls)): return args arg_spec = inspect.getfullargspec(cls.__init__) - return cls(*[_get_argument_value(arg_spec, index, args) for index in range(1, len(arg_spec.args))]) + return cls(*[_get_argument_value(cls, arg_spec, index, args) for index in range(1, len(arg_spec.args))]) -def _get_argument_value(arg_spec: FullArgSpec, index: int, parameters: dict): +def _get_argument_value(cls: Type[T], arg_spec: FullArgSpec, index: int, parameters: dict): argument_name = arg_spec.args[index] try: value = parameters[argument_name] except KeyError: - try: - value = arg_spec.defaults[-len(arg_spec.args) + index] - except (IndexError, TypeError): - # If none of the parameters have a default value, then `arg_spec.defaults` is None and we get a `TypeError`. - # If there are parameters with default value, but not the one requested, we get an `IndexError`. - raise Exception( - "Cannot determine value for parameter %s: not given in %s and no default value specified" % ( - argument_name, parameters)) + if is_dataclass(cls): + dataclass_field = next((field for field in fields(cls) if field.name == argument_name), None) + if dataclass_field is not None: + if dataclass_field.default_factory is not MISSING: + value = dataclass_field.default_factory() + elif dataclass_field.default is not MISSING: + value = dataclass_field.default + else: + raise Exception( + "Cannot determine value for parameter %s: not given in %s and no default value specified" % ( + argument_name, parameters)) + else: + raise Exception( + "Cannot determine value for parameter %s: not given in %s and no dataclass field found" % ( + argument_name, parameters)) + else: + try: + value = arg_spec.defaults[-len(arg_spec.args) + index] + except (IndexError, TypeError): + # If none of the parameters have a default value, then `arg_spec.defaults` is None and we get a `TypeError`. + # If there are parameters with default value, but not the one requested, we get an `IndexError`. + raise Exception( + "Cannot determine value for parameter %s: not given in %s and no default value specified" % ( + argument_name, parameters)) return _dataclass_from_dict_recurse(value, arg_spec.annotations.get(argument_name)) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index b2169ac36a..4919a77065 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 128 + DATASTORE_VERSION = 129 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3264,3 +3264,27 @@ def upgrade_component(topic: str, payload) -> Optional[dict]: ) self._append_datastore_version(128) + + def upgrade_datastore_129(self) -> None: + def upgrade(topic: str, payload) -> None: + if "openWB/optional/ep/flexible_tariff/provider" == topic: + provider = decode_payload(payload) + if provider["type"] == "fixed_hours": + if provider["configuration"].get("update_hours") is None: + provider["configuration"]["update_hours"] = list(range(24)) + return {topic: provider} + elif provider["type"] == "ostrom": + if provider["configuration"].get("update_hours") is None: + provider["configuration"]["update_hours"] = [2, 8, 14, 20] + return {topic: provider} + elif provider["type"] == "tibber": + if (provider["configuration"].get("update_hours") is None or + provider["configuration"].get("includes_grid_fee") is None): + if provider["configuration"].get("update_hours") is None: + provider["configuration"]["update_hours"] = [14] + if provider["configuration"].get("includes_grid_fee") is None: + provider["configuration"]["includes_grid_fee"] = True + return {topic: provider} + return None + self._loop_all_received_topics(upgrade) + # self._append_datastore_version(129) From 4717eba665cde737b63800cdf74837ac13e5df73 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 15 Jun 2026 11:11:26 +0200 Subject: [PATCH 2/3] flake8, remove coment --- packages/dataclass_utils/_dataclass_from_dict.py | 5 +++-- packages/helpermodules/update_config.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/dataclass_utils/_dataclass_from_dict.py b/packages/dataclass_utils/_dataclass_from_dict.py index 4e626d8a5c..eb9812c4b7 100644 --- a/packages/dataclass_utils/_dataclass_from_dict.py +++ b/packages/dataclass_utils/_dataclass_from_dict.py @@ -54,8 +54,9 @@ def _get_argument_value(cls: Type[T], arg_spec: FullArgSpec, index: int, paramet try: value = arg_spec.defaults[-len(arg_spec.args) + index] except (IndexError, TypeError): - # If none of the parameters have a default value, then `arg_spec.defaults` is None and we get a `TypeError`. - # If there are parameters with default value, but not the one requested, we get an `IndexError`. + # If none of the parameters have a default value, then `arg_spec.defaults` is None and we get a + # `TypeError`. If there are parameters with default value, but not the one requested, we get an + # `IndexError`. raise Exception( "Cannot determine value for parameter %s: not given in %s and no default value specified" % ( argument_name, parameters)) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 4919a77065..4d7e1407cb 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3287,4 +3287,4 @@ def upgrade(topic: str, payload) -> None: return {topic: provider} return None self._loop_all_received_topics(upgrade) - # self._append_datastore_version(129) + self._append_datastore_version(129) From 0a930b189976bd2943a1997aa53e9835e23d99bc Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 15 Jun 2026 11:15:09 +0200 Subject: [PATCH 3/3] add test --- .../_dataclass_from_dict_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/dataclass_utils/_dataclass_from_dict_test.py b/packages/dataclass_utils/_dataclass_from_dict_test.py index 47348d8dd7..1f0006e3bf 100644 --- a/packages/dataclass_utils/_dataclass_from_dict_test.py +++ b/packages/dataclass_utils/_dataclass_from_dict_test.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field from typing import Dict, Generic, Optional, Type, TypeVar import pytest @@ -42,6 +43,12 @@ def __init__(self, a: str, o: Dict[int, float] = None): self.o = o +@dataclass +class DataclassWithDefaultFactoryList: + values: list[int] = field(default_factory=lambda: [14]) + name: str = "default-name" + + def test_from_dict_simple(): # execution actual = dataclass_from_dict(SimpleSample, {"b": "bValue", "a": "aValue"}) @@ -128,6 +135,17 @@ def test_from_dict_without_optional(): assert actual.o is None +def test_from_dict_dataclass_uses_default_factory_if_key_missing(): + # execution + actual = dataclass_from_dict(DataclassWithDefaultFactoryList, {"name": "configured"}) + default_instance = DataclassWithDefaultFactoryList() + + # evaluation + assert actual.name == "configured" + assert actual.values == default_instance.values # vergleicht die Werte + assert actual.values is not default_instance.values # vergleicht die Instanzen + + MY_DATACLASS_AS_DICT = { "str_value": "string_value", "float_value": 5.2,