From 2a7f0e60fdc4a5d2c01c82d4d76233dce5338d47 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 20 Dec 2025 20:03:08 +0100 Subject: [PATCH 01/16] Add THERMO_MATCHING constant --- plugwise/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugwise/constants.py b/plugwise/constants.py index 1ddd37c5f..f495feb5b 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -92,6 +92,12 @@ NONE: Final = "None" OFF: Final = "off" PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central") +THERMO_MATCHING: dict[str, int] = { + "thermostat": 2, + "zone_thermometer": 2, + "zone_thermostat": 2, + "thermostatic_radiator_valve": 1, +} # XML data paths APPLIANCES: Final = "/core/appliances" From 762d16f89445c49429d09c6d76e3d3e9b6dc59ce Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 20 Dec 2025 20:09:48 +0100 Subject: [PATCH 02/16] Rename to _het_appliances(), _get_locations(), optimize _get_appliances() --- plugwise/helper.py | 55 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 0a05864ae..982a0c157 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -93,41 +93,43 @@ def item_count(self) -> int: """Return the item-count.""" return self._count - def _all_appliances(self) -> None: + def _get_appliances(self) -> None: """Collect all appliances with relevant info. Also, collect the P1 smartmeter info from a location as this one is not available as an appliance. """ self._count = 0 - self._all_locations() + self._get_locations() for appliance in self._domain_objects.findall("./appliance"): appl = Munch() + appl.available = None + appl.entity_id = appliance.attrib["id"] + appl.location = None + appl.name = appliance.find("name").text + appl.model = None + appl.model_id = None + appl.module_id = None + appl.firmware = None + appl.hardware = None + appl.mac = None appl.pwclass = appliance.find("type").text - # Don't collect data for the OpenThermGateway appliance - if appl.pwclass == "open_therm_gateway": - continue - - # Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739 - description = appliance.find("description").text - if description is not None and ( - "ZigBee protocol" in description or "smart plug" in description - ): - appl.pwclass = f"{appl.pwclass}_plug" + appl.zigbee_mac = None + appl.vendor_name = None - # Skip thermostats that have this key, should be an orphaned device (Core #81712) - if ( + # Don't collect data for the OpenThermGateway appliance, skip thermostat(s) + # without actuator_functionalities, should be an orphaned device(s) (Core #81712) + if appl.pwclass == "open_therm_gateway" or ( appl.pwclass == "thermostat" and appliance.find("actuator_functionalities/") is None ): continue - appl.location = None if (appl_loc := appliance.find("location")) is not None: appl.location = appl_loc.attrib["id"] - # Don't assign the _home_loc_id to thermostat-devices without a location, - # they are not active + # Set location to the _home_loc_id when the appliance-location is not found, + # except for thermostat-devices without a location, they are not active elif appl.pwclass not in THERMOSTAT_CLASSES: appl.location = self._home_loc_id @@ -135,16 +137,12 @@ def _all_appliances(self) -> None: if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None: continue - appl.available = None - appl.entity_id = appliance.attrib["id"] - appl.name = appliance.find("name").text - appl.model = None - appl.model_id = None - appl.firmware = None - appl.hardware = None - appl.mac = None - appl.zigbee_mac = None - appl.vendor_name = None + # Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739 + description = appliance.find("description").text + if description is not None and ( + "ZigBee protocol" in description or "smart plug" in description + ): + appl.pwclass = f"{appl.pwclass}_plug" # Collect appliance info, skip orphaned/removed devices if not (appl := self._appliance_info_finder(appl, appliance)): @@ -152,8 +150,9 @@ def _all_appliances(self) -> None: self._create_gw_entities(appl) + # A smartmeter is not present as an appliance, add it specifically if self.smile.type == "power" or self.smile.anna_p1: - self._get_p1_smartmeter_info() + self._add_p1_smartmeter_info() # Sort the gw_entities self._reorder_devices() From c9702bffa38da64d204328341d058c470a14d73b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 20 Dec 2025 20:11:35 +0100 Subject: [PATCH 03/16] Use THERMO_MATCHING constant --- plugwise/helper.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 982a0c157..4a73c8416 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -742,17 +742,9 @@ def _scan_thermostats(self) -> None: the result to update the device_class of secondary thermostats. """ self._thermo_locs = self._match_locations() - - thermo_matching: dict[str, int] = { - "thermostat": 2, - "zone_thermometer": 2, - "zone_thermostat": 2, - "thermostatic_radiator_valve": 1, - } - for loc_id in self._thermo_locs: for entity_id, entity in self.gw_entities.items(): - self._rank_thermostat(thermo_matching, loc_id, entity_id, entity) + self._rank_thermostat(THERMO_MATCHING, loc_id, entity_id, entity) for loc_id, loc_data in self._thermo_locs.items(): if loc_data["primary_prio"] != 0: From 8ad9481807f9d9985d7d1898df2b2bebeadc9439 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 20 Dec 2025 20:12:49 +0100 Subject: [PATCH 04/16] Rename _all_ to _get_ --- plugwise/helper.py | 3 ++- plugwise/smile.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 4a73c8416..de041b3ca 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -30,6 +30,7 @@ OFF, P1_MEASUREMENTS, TEMP_CELSIUS, + THERMO_MATCHING, THERMOSTAT_CLASSES, TOGGLES, UOM, @@ -193,7 +194,7 @@ def _get_p1_smartmeter_info(self) -> None: self._create_gw_entities(appl) - def _all_locations(self) -> None: + def _get_locations(self) -> None: """Collect all locations.""" loc = Munch() locations = self._domain_objects.findall("./location") diff --git a/plugwise/smile.py b/plugwise/smile.py index 22010af5c..1508f752e 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -107,7 +107,7 @@ def get_all_gateway_entities(self) -> None: Collect and add switching- and/or pump-group entities. Finally, collect the data and states for each entity. """ - self._all_appliances() + self._get_appliances() if self._is_thermostat: self.therms_with_offset_func = ( self._get_appliances_with_offset_functionality() From 6c71928c193165172344ede6ebd401728bb89150 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 20 Dec 2025 20:15:36 +0100 Subject: [PATCH 05/16] Rename to _get_p1_smartmeter_info() --- plugwise/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index de041b3ca..92cf86b9c 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -153,7 +153,7 @@ def _get_appliances(self) -> None: # A smartmeter is not present as an appliance, add it specifically if self.smile.type == "power" or self.smile.anna_p1: - self._add_p1_smartmeter_info() + self._get_p1_smartmeter_info() # Sort the gw_entities self._reorder_devices() From 531119864e12c22b4d16179cdb63acb8ed5004be Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 09:52:28 +0100 Subject: [PATCH 06/16] Rework _scan_thermostat() and related --- plugwise/helper.py | 101 ++++++++++++++++++++++----------------------- plugwise/smile.py | 4 +- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 92cf86b9c..04ad6f23d 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -199,16 +199,19 @@ def _get_locations(self) -> None: loc = Munch() locations = self._domain_objects.findall("./location") for location in locations: - loc.name = location.find("name").text loc.loc_id = location.attrib["id"] - self._loc_data[loc.loc_id] = {"name": loc.name} - if loc.name != "Home": - continue - - self._home_loc_id = loc.loc_id - self._home_location = self._domain_objects.find( - f"./location[@id='{loc.loc_id}']" - ) + loc.name = location.find("name").text + self._loc_data[loc.loc_id] = { + "name": loc.name, + "primary": [], + "primary_prio": 0, + "secondary": [], + } + if loc.name == "Home": + self._home_loc_id = loc.loc_id + self._home_location = self._domain_objects.find( + f"./location[@id='{loc.loc_id}']" + ) def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch: """Collect info for all appliances found.""" @@ -739,76 +742,72 @@ def _cleanup_data(self, data: GwEntityData) -> None: def _scan_thermostats(self) -> None: """Helper-function for smile.py: get_all_entities(). - Update locations with thermostat ranking results and use + Adam only: update locations with thermostat ranking results and use the result to update the device_class of secondary thermostats. """ - self._thermo_locs = self._match_locations() - for loc_id in self._thermo_locs: - for entity_id, entity in self.gw_entities.items(): - self._rank_thermostat(THERMO_MATCHING, loc_id, entity_id, entity) + if not self.check_name(ADAM): + return - for loc_id, loc_data in self._thermo_locs.items(): - if loc_data["primary_prio"] != 0: - self._zones[loc_id] = { + self._match_and_rank_thermostats() + for location_id, location in self._loc_data.items(): + if location["primary_prio"] != 0: + self._zones[location_id] = { "dev_class": "climate", "model": "ThermoZone", - "name": loc_data["name"], + "name": location["name"], "thermostats": { - "primary": loc_data["primary"], - "secondary": loc_data["secondary"], + "primary": location["primary"], + "secondary": location["secondary"], }, "vendor": "Plugwise", } self._count += 5 - def _match_locations(self) -> dict[str, ThermoLoc]: + def _match_and_rank_thermostats(self) -> None: """Helper-function for _scan_thermostats(). - Match appliances with locations. + Match thermostat-appliances with locations, rank them for locations with multiple thermostats. """ - matched_locations: dict[str, ThermoLoc] = {} - for location_id, location_details in self._loc_data.items(): - for appliance_details in self.gw_entities.values(): - if appliance_details["location"] == location_id: - location_details.update( - {"primary": [], "primary_prio": 0, "secondary": []} - ) - matched_locations[location_id] = location_details - - return matched_locations + for location_id, location in self._loc_data.items(): + for entity_id, entity in self.gw_entities.items(): + self._rank_thermostat( + entity_id, entity, location_id, location, THERMO_MATCHING + ) def _rank_thermostat( self, + entity_id: str, + entity: GwEntityData, + location_id: str, + location: ThermoLoc, thermo_matching: dict[str, int], - loc_id: str, - appliance_id: str, - appliance_details: GwEntityData, ) -> None: """Helper-function for _scan_thermostats(). - Rank the thermostat based on appliance_details: primary or secondary. - Note: there can be several primary and secondary thermostats. + Rank the thermostat based on entity-thermostat-type: primary or secondary. + There can be several primary and secondary thermostats per location. """ - appl_class = appliance_details["dev_class"] - appl_d_loc = appliance_details["location"] - thermo_loc = self._thermo_locs[loc_id] - if loc_id == appl_d_loc and appl_class in thermo_matching: - if thermo_matching[appl_class] == thermo_loc["primary_prio"]: - thermo_loc["primary"].append(appliance_id) + appl_class = entity["dev_class"] + if ( + "location" in entity + and location_id == entity["location"] + and appl_class in thermo_matching + ): # Pre-elect new primary - elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc[ + if thermo_matching[appl_class] == location["primary_prio"]: + location["primary"].append(entity_id) + elif (thermo_rank := thermo_matching[appl_class]) > location[ "primary_prio" ]: - thermo_loc["primary_prio"] = thermo_rank + location["primary_prio"] = thermo_rank # Demote former primary - if tl_primary := thermo_loc["primary"]: - thermo_loc["secondary"] += tl_primary - thermo_loc["primary"] = [] - + if tl_primary := location["primary"]: + location["secondary"] += tl_primary + location["primary"] = [] # Crown primary - thermo_loc["primary"].append(appliance_id) + location["primary"].append(entity_id) else: - thermo_loc["secondary"].append(appliance_id) + location["secondary"].append(entity_id) def _control_state(self, data: GwEntityData) -> str | bool: """Helper-function for _get_location_data(). diff --git a/plugwise/smile.py b/plugwise/smile.py index 1508f752e..058d767b8 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -10,7 +10,6 @@ from typing import Any, cast from plugwise.constants import ( - ADAM, ALLOWED_ZONE_PROFILES, ANNA, APPLIANCES, @@ -112,8 +111,7 @@ def get_all_gateway_entities(self) -> None: self.therms_with_offset_func = ( self._get_appliances_with_offset_functionality() ) - if self.check_name(ADAM): - self._scan_thermostats() + self._scan_thermostats() if group_data := self._get_groups(): self.gw_entities.update(group_data) From 758b418e3265787a0a9bd8143fda22f732aaaf8a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 10:16:48 +0100 Subject: [PATCH 07/16] Type added constant with Final as suggested --- plugwise/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index f495feb5b..188a23bfc 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -92,7 +92,7 @@ NONE: Final = "None" OFF: Final = "off" PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central") -THERMO_MATCHING: dict[str, int] = { +THERMO_MATCHING: Final[dict[str, int]] = { "thermostat": 2, "zone_thermometer": 2, "zone_thermostat": 2, From 034d9485e169158bc10dbd577c85b96ad129d32e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 10:22:50 +0100 Subject: [PATCH 08/16] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52eff569..7b52b0def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Ongoing -- Code optimizations via PR [#837](https://github.com/plugwise/python-plugwise/pull/837) +- Code optimizations via PR [#837](https://github.com/plugwise/python-plugwise/pull/837), [#838](https://github.com/plugwise/python-plugwise/pull/838) ## v1.11.0 From 33e1e4db143420e9c1343794597c80e9df9e4729 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 10:38:04 +0100 Subject: [PATCH 09/16] Break out extend_plug_device_class() function --- plugwise/helper.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 04ad6f23d..6dfc48543 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -57,6 +57,17 @@ from packaging import version +def extend_plug_device_class(appl: Munch, appliance: etree.Element) -> None: + """Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739.""" + + if ( + (search := appliance.find("description")) is not None + and (description := search.text) is not None + and ("ZigBee protocol" in description or "smart plug" in description) + ): + appl.pwclass = f"{appl.pwclass}_plug" + + def search_actuator_functionalities( appliance: etree.Element, actuator: str ) -> etree.Element | None: @@ -138,12 +149,7 @@ def _get_appliances(self) -> None: if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None: continue - # Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739 - description = appliance.find("description").text - if description is not None and ( - "ZigBee protocol" in description or "smart plug" in description - ): - appl.pwclass = f"{appl.pwclass}_plug" + extend_plug_device_class(appl, appliance) # Collect appliance info, skip orphaned/removed devices if not (appl := self._appliance_info_finder(appl, appliance)): From 6a414862d5057a1390d92866add399d695f8b1bd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 11:06:56 +0100 Subject: [PATCH 10/16] Improve _match_and_rank_thermostats() as suggested --- plugwise/helper.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 6dfc48543..f42327ec2 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -59,7 +59,7 @@ def extend_plug_device_class(appl: Munch, appliance: etree.Element) -> None: """Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739.""" - + if ( (search := appliance.find("description")) is not None and (description := search.text) is not None @@ -774,8 +774,16 @@ def _match_and_rank_thermostats(self) -> None: Match thermostat-appliances with locations, rank them for locations with multiple thermostats. """ + # Build location index + entities_by_location: dict[str, list[tuple[str, GwEntityData]]] = {} + for entity_id, entity in self.gw_entities.items(): + if "location" in entity: + loc = entity["location"] + entities_by_location.setdefault(loc, []).append((entity_id, entity)) + + # Rank thermostats per location for location_id, location in self._loc_data.items(): - for entity_id, entity in self.gw_entities.items(): + for entity_id, entity in entities_by_location.get(location_id, []): self._rank_thermostat( entity_id, entity, location_id, location, THERMO_MATCHING ) From 3400aad758932252cca1c0891eb42719d5b592e2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 11:18:53 +0100 Subject: [PATCH 11/16] Optimize as suggested --- plugwise/helper.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index f42327ec2..ffb8283e0 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -801,12 +801,7 @@ def _rank_thermostat( Rank the thermostat based on entity-thermostat-type: primary or secondary. There can be several primary and secondary thermostats per location. """ - appl_class = entity["dev_class"] - if ( - "location" in entity - and location_id == entity["location"] - and appl_class in thermo_matching - ): + if (appl_class := entity["dev_class"]) in thermo_matching: # Pre-elect new primary if thermo_matching[appl_class] == location["primary_prio"]: location["primary"].append(entity_id) From 8a4202b4f0b6fa7760baf877c4b95585dc6b5ea5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 11:34:30 +0100 Subject: [PATCH 12/16] Improve detection and error-handling for home-location --- plugwise/helper.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index ffb8283e0..16b32ff16 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -202,23 +202,32 @@ def _get_p1_smartmeter_info(self) -> None: def _get_locations(self) -> None: """Collect all locations.""" + counter = 0 loc = Munch() locations = self._domain_objects.findall("./location") for location in locations: loc.loc_id = location.attrib["id"] loc.name = location.find("name").text + loc._type = location.find("type").text self._loc_data[loc.loc_id] = { "name": loc.name, "primary": [], "primary_prio": 0, "secondary": [], } - if loc.name == "Home": + # Home location is of type building + if loc._type == "building": + counter += 1 self._home_loc_id = loc.loc_id self._home_location = self._domain_objects.find( f"./location[@id='{loc.loc_id}']" ) + if counter == 0: + raise KeyError( + "Error, location Home (building) not found!" + ) # pragma: no cover + def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch: """Collect info for all appliances found.""" match appl.pwclass: From 0c4271c2d9f5a847bc879a1bdd5f7ded97457882 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 11:45:44 +0100 Subject: [PATCH 13/16] Adapt for legacy location too --- plugwise/legacy/helper.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugwise/legacy/helper.py b/plugwise/legacy/helper.py index 112ef6e8e..ad0120e3b 100644 --- a/plugwise/legacy/helper.py +++ b/plugwise/legacy/helper.py @@ -148,18 +148,15 @@ def _all_locations(self) -> None: return for location in locations: - loc.name = location.find("name").text loc.loc_id = location.attrib["id"] + loc.name = location.find("name").text + loc._type = location.find("type").text # Filter the valid single location for P1 legacy: services not empty locator = "./services" if self.smile.type == "power" and len(location.find(locator)) == 0: continue - if loc.name == "Home": - self._home_loc_id = loc.loc_id - # Replace location-name for P1 legacy, can contain privacy-related info - if self.smile.type == "power": - loc.name = "Home" + if loc._type == "building": self._home_loc_id = loc.loc_id self._loc_data[loc.loc_id] = {"name": loc.name} From 700073e37124fb33929187dce40580ea0b7ffe8c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 11:48:57 +0100 Subject: [PATCH 14/16] Clean up --- plugwise/helper.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 16b32ff16..d8b9deff1 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -793,15 +793,12 @@ def _match_and_rank_thermostats(self) -> None: # Rank thermostats per location for location_id, location in self._loc_data.items(): for entity_id, entity in entities_by_location.get(location_id, []): - self._rank_thermostat( - entity_id, entity, location_id, location, THERMO_MATCHING - ) + self._rank_thermostat(entity_id, entity, location, THERMO_MATCHING) def _rank_thermostat( self, entity_id: str, entity: GwEntityData, - location_id: str, location: ThermoLoc, thermo_matching: dict[str, int], ) -> None: From 86a33b5b7b0c057e9566e81437f38fd94bf444b0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 12:29:35 +0100 Subject: [PATCH 15/16] Use negative as suggested --- plugwise/helper.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index d8b9deff1..040a34dc3 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -807,22 +807,24 @@ def _rank_thermostat( Rank the thermostat based on entity-thermostat-type: primary or secondary. There can be several primary and secondary thermostats per location. """ - if (appl_class := entity["dev_class"]) in thermo_matching: - # Pre-elect new primary - if thermo_matching[appl_class] == location["primary_prio"]: - location["primary"].append(entity_id) - elif (thermo_rank := thermo_matching[appl_class]) > location[ - "primary_prio" - ]: - location["primary_prio"] = thermo_rank - # Demote former primary - if tl_primary := location["primary"]: - location["secondary"] += tl_primary - location["primary"] = [] - # Crown primary - location["primary"].append(entity_id) - else: - location["secondary"].append(entity_id) + if (appl_class := entity["dev_class"]) not in thermo_matching: + return None + + # Pre-elect new primary + if thermo_matching[appl_class] == location["primary_prio"]: + location["primary"].append(entity_id) + elif (thermo_rank := thermo_matching[appl_class]) > location[ + "primary_prio" + ]: + location["primary_prio"] = thermo_rank + # Demote former primary + if tl_primary := location["primary"]: + location["secondary"] += tl_primary + location["primary"] = [] + # Crown primary + location["primary"].append(entity_id) + else: + location["secondary"].append(entity_id) def _control_state(self, data: GwEntityData) -> str | bool: """Helper-function for _get_location_data(). From e4efbbe688b7cf800b8187be7e6696969d328b21 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 21 Dec 2025 12:49:20 +0100 Subject: [PATCH 16/16] Revert back to short solution --- plugwise/helper.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/plugwise/helper.py b/plugwise/helper.py index 040a34dc3..f62e395a7 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -783,22 +783,17 @@ def _match_and_rank_thermostats(self) -> None: Match thermostat-appliances with locations, rank them for locations with multiple thermostats. """ - # Build location index - entities_by_location: dict[str, list[tuple[str, GwEntityData]]] = {} - for entity_id, entity in self.gw_entities.items(): - if "location" in entity: - loc = entity["location"] - entities_by_location.setdefault(loc, []).append((entity_id, entity)) - - # Rank thermostats per location for location_id, location in self._loc_data.items(): - for entity_id, entity in entities_by_location.get(location_id, []): - self._rank_thermostat(entity_id, entity, location, THERMO_MATCHING) + for entity_id, entity in self.gw_entities.items(): + self._rank_thermostat( + entity_id, entity, location_id, location, THERMO_MATCHING + ) def _rank_thermostat( self, entity_id: str, entity: GwEntityData, + location_id: str, location: ThermoLoc, thermo_matching: dict[str, int], ) -> None: @@ -807,15 +802,17 @@ def _rank_thermostat( Rank the thermostat based on entity-thermostat-type: primary or secondary. There can be several primary and secondary thermostats per location. """ - if (appl_class := entity["dev_class"]) not in thermo_matching: + if not ( + "location" in entity + and location_id == entity["location"] + and (appl_class := entity["dev_class"]) in thermo_matching + ): return None # Pre-elect new primary if thermo_matching[appl_class] == location["primary_prio"]: location["primary"].append(entity_id) - elif (thermo_rank := thermo_matching[appl_class]) > location[ - "primary_prio" - ]: + elif (thermo_rank := thermo_matching[appl_class]) > location["primary_prio"]: location["primary_prio"] = thermo_rank # Demote former primary if tl_primary := location["primary"]: