diff --git a/packages/control/algorithm/filter_chargepoints.py b/packages/control/algorithm/filter_chargepoints.py index 7454fe9a83..b1490954db 100644 --- a/packages/control/algorithm/filter_chargepoints.py +++ b/packages/control/algorithm/filter_chargepoints.py @@ -32,6 +32,20 @@ def _is_valid_for_chargemode(entity: Load, return _group_loads_by_chargemode(chargemodes, _is_valid_for_chargemode)[1] +def _get_consumer_by_prio_item(item: dict) -> Optional[Consumer]: + """Liefert den Verbraucher zu einem Eintrag der Prioritätensteuerung. + + Ein Eintrag kann kurzzeitig auf einen bereits gelöschten Verbraucher verweisen, wenn dessen Entfernung + aus der Prioritätensteuerung fehlgeschlagen ist. Ein einzelner solcher Karteileichen-Eintrag darf nicht + die komplette Regelung für alle Ladepunkte zum Absturz bringen. + """ + consumer = data.data.consumer_data.get(f"{item['type']}{item['id']}") + if consumer is None: + log.warning(f"Verbraucher {item['id']} aus der Prioritätensteuerung existiert nicht (mehr), " + "wird ignoriert.") + return consumer + + def _group_loads_by_chargemode(chargemodes: Tuple[Tuple[Optional[str], str]], filter_func) -> Tuple[List[Load], List[List[Load]]]: grouped_loads: List[List[Load]] = [] @@ -48,8 +62,8 @@ def _group_loads_by_chargemode(chargemodes: Tuple[Tuple[Optional[str], str]], sub_valid_chargemode.append(cp) flat_loads.append(cp) elif group_item["type"] == "consumer": - consumer = data.data.consumer_data[f"{group_item['type']}{group_item['id']}"] - if filter_func(consumer, chargemode, flat_loads): + consumer = _get_consumer_by_prio_item(group_item) + if consumer is not None and filter_func(consumer, chargemode, flat_loads): sub_valid_chargemode.append(consumer) flat_loads.append(consumer) grouped_loads.append(sub_valid_chargemode) @@ -60,8 +74,8 @@ def _group_loads_by_chargemode(chargemodes: Tuple[Tuple[Optional[str], str]], grouped_loads.append([cp]) flat_loads.append(cp) elif item["type"] == "consumer": - consumer = data.data.consumer_data[f"{item['type']}{item['id']}"] - if filter_func(consumer, chargemode, flat_loads): + consumer = _get_consumer_by_prio_item(item) + if consumer is not None and filter_func(consumer, chargemode, flat_loads): grouped_loads.append([consumer]) flat_loads.append(consumer) return flat_loads, grouped_loads diff --git a/packages/control/algorithm/filter_chargepoints_test.py b/packages/control/algorithm/filter_chargepoints_test.py index 1cd469a374..bf42f2759a 100644 --- a/packages/control/algorithm/filter_chargepoints_test.py +++ b/packages/control/algorithm/filter_chargepoints_test.py @@ -68,6 +68,25 @@ def mock_consumer3() -> Consumer: {"type": "consumer", "id": 3}, {"type": "vehicle", "id": 2}], [1, 3, 2], id="required current 0 for cp 1"), + pytest.param(6, 1, ((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING),), + [{"type": "vehicle", "id": 1}, + # Verbraucher 99 wurde gelöscht, ist aber (Bug) noch in der Prioritätensteuerung + # gelistet. Darf die Ermittlung der anderen Lasten nicht zum Absturz bringen. + {"type": "consumer", "id": 99}, + {"type": "vehicle", "id": 2}, + {"type": "consumer", "id": 3}], + [1, 3], id="stale consumer reference is skipped, not fatal"), + pytest.param(6, 1, ((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING),), + [{"type": "vehicle", "id": 1}, + { + "type": "group", + "label": "Gruppe 1", + "children": [ + {"type": "vehicle", "id": 2}, + # ebenfalls gelöschter, aber noch gelisteter Verbraucher, diesmal in einer Gruppe + {"type": "consumer", "id": 99}, + {"type": "consumer", "id": 3}]}], + [1, 3], id="stale consumer reference in group is skipped, not fatal"), ]) def test_get_loads_by_chargemodes( required_current_1: int, diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py index 80b94dfc1b..5bfe377d6d 100644 --- a/packages/control/counter_all/loadmanagement_prio.py +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -15,7 +15,9 @@ def add_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: Compone def remove_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, id: int) -> None: if self._remove_loadmanagement_prio_item(type, id, self.data.get.loadmanagement_prios) is False: - raise IndexError(f"Element {id} konnte nicht in der Prioritätensteuerung gefunden werden.") + # Kein Grund, die Löschung des restlichen Elements (Topics, Hierarchie) abzubrechen, wenn der + # Eintrag in der Prioritätensteuerung schon fehlt. + log.warning(f"Element {type.value}/{id} konnte nicht in der Prioritätensteuerung gefunden werden.") def _remove_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, diff --git a/packages/control/counter_all/loadmanagement_prio_test.py b/packages/control/counter_all/loadmanagement_prio_test.py index 4bdd0a1ff2..5bb780ed92 100644 --- a/packages/control/counter_all/loadmanagement_prio_test.py +++ b/packages/control/counter_all/loadmanagement_prio_test.py @@ -83,6 +83,9 @@ def test_add_item(loadmanagement_prios: List[Dict], ComponentType.VEHICLE, 2, [{"type": "vehicle", "id": 3}], id="flat list, remove vehicle"), pytest.param([{"type": "vehicle", "id": 3}, {"type": "consumer", "id": 2}], ComponentType.CONSUMER, 2, [{"type": "vehicle", "id": 3}], id="flat list, remove consumer"), + pytest.param([{"type": "vehicle", "id": 3}, {"type": "consumer", "id": 2}], + ComponentType.CONSUMER, 99, [{"type": "vehicle", "id": 3}, {"type": "consumer", "id": 2}], + id="id not present, must not raise and must not change the list"), pytest.param([ { "type": "group", diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index bb7b779858..8cb2fed9d3 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -1248,6 +1248,7 @@ def process_consumer_topic(self, msg): "openWB/set/consumer/get/daily_exported" in msg.topic): self._validate_value(msg, float) elif (re.search("openWB/set/consumer/[0-9]+/module$", msg.topic) is not None or + re.search("openWB/set/consumer/[0-9]+/module/simulation$", msg.topic) is not None or re.search("openWB/set/consumer/[0-9]+/config$", msg.topic) is not None or re.search("openWB/set/consumer/[0-9]+/usage$", msg.topic) is not None): self._validate_value(msg, "json") diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index b994423d23..a115217f78 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1248,14 +1248,25 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], else: if f"consumer{index}" not in var: var[f"consumer{index}"] = Consumer(int(index)) - if re.search("openWB/consumer/[0-9]+/module", msg.topic) is not None: + if re.search("openWB/consumer/[0-9]+/module$", msg.topic) is not None: consumer_config = decode_payload(msg.payload) - con = importlib.import_module( - f".consumers.{consumer_config['vendor']}.{consumer_config['type']}.consumer", - "modules") - config = dataclass_from_dict(con.device_descriptor.configuration_factory, consumer_config) - var["consumer"+index].module = con.create_consumer(config) - var["consumer"+index].data.module = config + try: + con = importlib.import_module( + f".consumers.{consumer_config['vendor']}.{consumer_config['type']}.consumer", + "modules") + config = dataclass_from_dict( + con.device_descriptor.configuration_factory, consumer_config) + var["consumer"+index].module = con.create_consumer(config) + var["consumer"+index].data.module = config + except Exception: + fault_str = ( + f"Verbraucher {index}: Modul für " + f"{consumer_config.get('vendor')}/{consumer_config.get('type')} konnte nicht " + "erstellt werden, siehe Log.") + log.exception(fault_str) + var["consumer"+index].data.get.fault_state = 2 + var["consumer"+index].data.get.fault_str = fault_str + pub_system_message({}, fault_str, MessageType.ERROR) elif re.search("openWB/consumer/[0-9]+/config", msg.topic) is not None: self.set_json_payload_class(var["consumer"+index].data.config, msg) elif re.search("openWB/consumer/[0-9]+/get", msg.topic) is not None: diff --git a/packages/modules/loadvars.py b/packages/modules/loadvars.py index 81e9e6b187..5e8004a7c8 100644 --- a/packages/modules/loadvars.py +++ b/packages/modules/loadvars.py @@ -64,6 +64,9 @@ def _set_values(self) -> List[str]: log.exception(f"Fehler im loadvars-Modul bei Element {cp.num}") for consumer in data.data.consumer_data.values(): try: + if consumer.module is None: + # Verbraucher, dessen Modul nicht erstellt werden konnte (z.B. defekte Konfiguration). + continue modules_threads.append(Thread(target=consumer.module.update, args=(), name=f"set values consumer{consumer.data.module.id}")) @@ -113,6 +116,9 @@ def _update_values_of_level_buttom_top(self, elements, not_finished_threads: Lis name=f"update values cp{chargepoint.chargepoint_module.config.id}")) elif element["type"] == ComponentType.CONSUMER.value: consumer = data.data.consumer_data[f'{type_to_topic_mapping(element["type"])}{element["id"]}'] + if consumer.module is None: + # Verbraucher, dessen Modul nicht erstellt werden konnte (z.B. defekte Konfiguration). + continue thread_name = f"set values consumer{consumer.data.module.id}" if thread_name not in not_finished_threads: modules_threads.append(Thread(