Skip to content

Commit c005c19

Browse files
authored
Merge pull request #838 from plugwise/correct-group-related
Further code optimizations
2 parents 20b7108 + e4efbbe commit c005c19

File tree

5 files changed

+109
-103
lines changed

5 files changed

+109
-103
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Ongoing
44

5-
- Code optimizations via PR [#837](https://github.com/plugwise/python-plugwise/pull/837)
5+
- Code optimizations via PR [#837](https://github.com/plugwise/python-plugwise/pull/837), [#838](https://github.com/plugwise/python-plugwise/pull/838)
66

77
## v1.11.0
88

plugwise/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
NONE: Final = "None"
9393
OFF: Final = "off"
9494
PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central")
95+
THERMO_MATCHING: Final[dict[str, int]] = {
96+
"thermostat": 2,
97+
"zone_thermometer": 2,
98+
"zone_thermostat": 2,
99+
"thermostatic_radiator_valve": 1,
100+
}
95101

96102
# XML data paths
97103
APPLIANCES: Final = "/core/appliances"

plugwise/helper.py

Lines changed: 97 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
OFF,
3131
P1_MEASUREMENTS,
3232
TEMP_CELSIUS,
33+
THERMO_MATCHING,
3334
THERMOSTAT_CLASSES,
3435
TOGGLES,
3536
UOM,
@@ -56,6 +57,17 @@
5657
from packaging import version
5758

5859

60+
def extend_plug_device_class(appl: Munch, appliance: etree.Element) -> None:
61+
"""Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739."""
62+
63+
if (
64+
(search := appliance.find("description")) is not None
65+
and (description := search.text) is not None
66+
and ("ZigBee protocol" in description or "smart plug" in description)
67+
):
68+
appl.pwclass = f"{appl.pwclass}_plug"
69+
70+
5971
def search_actuator_functionalities(
6072
appliance: etree.Element, actuator: str
6173
) -> etree.Element | None:
@@ -93,65 +105,59 @@ def item_count(self) -> int:
93105
"""Return the item-count."""
94106
return self._count
95107

96-
def _all_appliances(self) -> None:
108+
def _get_appliances(self) -> None:
97109
"""Collect all appliances with relevant info.
98110
99111
Also, collect the P1 smartmeter info from a location
100112
as this one is not available as an appliance.
101113
"""
102114
self._count = 0
103-
self._all_locations()
115+
self._get_locations()
104116

105117
for appliance in self._domain_objects.findall("./appliance"):
106118
appl = Munch()
119+
appl.available = None
120+
appl.entity_id = appliance.attrib["id"]
121+
appl.location = None
122+
appl.name = appliance.find("name").text
123+
appl.model = None
124+
appl.model_id = None
125+
appl.module_id = None
126+
appl.firmware = None
127+
appl.hardware = None
128+
appl.mac = None
107129
appl.pwclass = appliance.find("type").text
108-
# Don't collect data for the OpenThermGateway appliance
109-
if appl.pwclass == "open_therm_gateway":
110-
continue
111-
112-
# Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739
113-
description = appliance.find("description").text
114-
if description is not None and (
115-
"ZigBee protocol" in description or "smart plug" in description
116-
):
117-
appl.pwclass = f"{appl.pwclass}_plug"
130+
appl.zigbee_mac = None
131+
appl.vendor_name = None
118132

119-
# Skip thermostats that have this key, should be an orphaned device (Core #81712)
120-
if (
133+
# Don't collect data for the OpenThermGateway appliance, skip thermostat(s)
134+
# without actuator_functionalities, should be an orphaned device(s) (Core #81712)
135+
if appl.pwclass == "open_therm_gateway" or (
121136
appl.pwclass == "thermostat"
122137
and appliance.find("actuator_functionalities/") is None
123138
):
124139
continue
125140

126-
appl.location = None
127141
if (appl_loc := appliance.find("location")) is not None:
128142
appl.location = appl_loc.attrib["id"]
129-
# Don't assign the _home_loc_id to thermostat-devices without a location,
130-
# they are not active
143+
# Set location to the _home_loc_id when the appliance-location is not found,
144+
# except for thermostat-devices without a location, they are not active
131145
elif appl.pwclass not in THERMOSTAT_CLASSES:
132146
appl.location = self._home_loc_id
133147

134148
# Don't show orphaned thermostat-types
135149
if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None:
136150
continue
137151

138-
appl.available = None
139-
appl.entity_id = appliance.attrib["id"]
140-
appl.name = appliance.find("name").text
141-
appl.model = None
142-
appl.model_id = None
143-
appl.firmware = None
144-
appl.hardware = None
145-
appl.mac = None
146-
appl.zigbee_mac = None
147-
appl.vendor_name = None
152+
extend_plug_device_class(appl, appliance)
148153

149154
# Collect appliance info, skip orphaned/removed devices
150155
if not (appl := self._appliance_info_finder(appl, appliance)):
151156
continue
152157

153158
self._create_gw_entities(appl)
154159

160+
# A smartmeter is not present as an appliance, add it specifically
155161
if self.smile.type == "power" or self.smile.anna_p1:
156162
self._get_p1_smartmeter_info()
157163

@@ -194,21 +200,33 @@ def _get_p1_smartmeter_info(self) -> None:
194200

195201
self._create_gw_entities(appl)
196202

197-
def _all_locations(self) -> None:
203+
def _get_locations(self) -> None:
198204
"""Collect all locations."""
205+
counter = 0
199206
loc = Munch()
200207
locations = self._domain_objects.findall("./location")
201208
for location in locations:
202-
loc.name = location.find("name").text
203209
loc.loc_id = location.attrib["id"]
204-
self._loc_data[loc.loc_id] = {"name": loc.name}
205-
if loc.name != "Home":
206-
continue
210+
loc.name = location.find("name").text
211+
loc._type = location.find("type").text
212+
self._loc_data[loc.loc_id] = {
213+
"name": loc.name,
214+
"primary": [],
215+
"primary_prio": 0,
216+
"secondary": [],
217+
}
218+
# Home location is of type building
219+
if loc._type == "building":
220+
counter += 1
221+
self._home_loc_id = loc.loc_id
222+
self._home_location = self._domain_objects.find(
223+
f"./location[@id='{loc.loc_id}']"
224+
)
207225

208-
self._home_loc_id = loc.loc_id
209-
self._home_location = self._domain_objects.find(
210-
f"./location[@id='{loc.loc_id}']"
211-
)
226+
if counter == 0:
227+
raise KeyError(
228+
"Error, location Home (building) not found!"
229+
) # pragma: no cover
212230

213231
def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch:
214232
"""Collect info for all appliances found."""
@@ -739,84 +757,71 @@ def _cleanup_data(self, data: GwEntityData) -> None:
739757
def _scan_thermostats(self) -> None:
740758
"""Helper-function for smile.py: get_all_entities().
741759
742-
Update locations with thermostat ranking results and use
760+
Adam only: update locations with thermostat ranking results and use
743761
the result to update the device_class of secondary thermostats.
744762
"""
745-
self._thermo_locs = self._match_locations()
746-
747-
thermo_matching: dict[str, int] = {
748-
"thermostat": 2,
749-
"zone_thermometer": 2,
750-
"zone_thermostat": 2,
751-
"thermostatic_radiator_valve": 1,
752-
}
753-
754-
for loc_id in self._thermo_locs:
755-
for entity_id, entity in self.gw_entities.items():
756-
self._rank_thermostat(thermo_matching, loc_id, entity_id, entity)
763+
if not self.check_name(ADAM):
764+
return
757765

758-
for loc_id, loc_data in self._thermo_locs.items():
759-
if loc_data["primary_prio"] != 0:
760-
self._zones[loc_id] = {
766+
self._match_and_rank_thermostats()
767+
for location_id, location in self._loc_data.items():
768+
if location["primary_prio"] != 0:
769+
self._zones[location_id] = {
761770
"dev_class": "climate",
762771
"model": "ThermoZone",
763-
"name": loc_data["name"],
772+
"name": location["name"],
764773
"thermostats": {
765-
"primary": loc_data["primary"],
766-
"secondary": loc_data["secondary"],
774+
"primary": location["primary"],
775+
"secondary": location["secondary"],
767776
},
768777
"vendor": "Plugwise",
769778
}
770779
self._count += 5
771780

772-
def _match_locations(self) -> dict[str, ThermoLoc]:
781+
def _match_and_rank_thermostats(self) -> None:
773782
"""Helper-function for _scan_thermostats().
774783
775-
Match appliances with locations.
784+
Match thermostat-appliances with locations, rank them for locations with multiple thermostats.
776785
"""
777-
matched_locations: dict[str, ThermoLoc] = {}
778-
for location_id, location_details in self._loc_data.items():
779-
for appliance_details in self.gw_entities.values():
780-
if appliance_details["location"] == location_id:
781-
location_details.update(
782-
{"primary": [], "primary_prio": 0, "secondary": []}
783-
)
784-
matched_locations[location_id] = location_details
785-
786-
return matched_locations
786+
for location_id, location in self._loc_data.items():
787+
for entity_id, entity in self.gw_entities.items():
788+
self._rank_thermostat(
789+
entity_id, entity, location_id, location, THERMO_MATCHING
790+
)
787791

788792
def _rank_thermostat(
789793
self,
794+
entity_id: str,
795+
entity: GwEntityData,
796+
location_id: str,
797+
location: ThermoLoc,
790798
thermo_matching: dict[str, int],
791-
loc_id: str,
792-
appliance_id: str,
793-
appliance_details: GwEntityData,
794799
) -> None:
795800
"""Helper-function for _scan_thermostats().
796801
797-
Rank the thermostat based on appliance_details: primary or secondary.
798-
Note: there can be several primary and secondary thermostats.
802+
Rank the thermostat based on entity-thermostat-type: primary or secondary.
803+
There can be several primary and secondary thermostats per location.
799804
"""
800-
appl_class = appliance_details["dev_class"]
801-
appl_d_loc = appliance_details["location"]
802-
thermo_loc = self._thermo_locs[loc_id]
803-
if loc_id == appl_d_loc and appl_class in thermo_matching:
804-
if thermo_matching[appl_class] == thermo_loc["primary_prio"]:
805-
thermo_loc["primary"].append(appliance_id)
806-
# Pre-elect new primary
807-
elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc[
808-
"primary_prio"
809-
]:
810-
thermo_loc["primary_prio"] = thermo_rank
811-
# Demote former primary
812-
if tl_primary := thermo_loc["primary"]:
813-
thermo_loc["secondary"] += tl_primary
814-
thermo_loc["primary"] = []
815-
816-
# Crown primary
817-
thermo_loc["primary"].append(appliance_id)
818-
else:
819-
thermo_loc["secondary"].append(appliance_id)
805+
if not (
806+
"location" in entity
807+
and location_id == entity["location"]
808+
and (appl_class := entity["dev_class"]) in thermo_matching
809+
):
810+
return None
811+
812+
# Pre-elect new primary
813+
if thermo_matching[appl_class] == location["primary_prio"]:
814+
location["primary"].append(entity_id)
815+
elif (thermo_rank := thermo_matching[appl_class]) > location["primary_prio"]:
816+
location["primary_prio"] = thermo_rank
817+
# Demote former primary
818+
if tl_primary := location["primary"]:
819+
location["secondary"] += tl_primary
820+
location["primary"] = []
821+
# Crown primary
822+
location["primary"].append(entity_id)
823+
else:
824+
location["secondary"].append(entity_id)
820825

821826
def _control_state(self, data: GwEntityData) -> str | bool:
822827
"""Helper-function for _get_location_data().

plugwise/legacy/helper.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,15 @@ def _all_locations(self) -> None:
148148
return
149149

150150
for location in locations:
151-
loc.name = location.find("name").text
152151
loc.loc_id = location.attrib["id"]
152+
loc.name = location.find("name").text
153+
loc._type = location.find("type").text
153154
# Filter the valid single location for P1 legacy: services not empty
154155
locator = "./services"
155156
if self.smile.type == "power" and len(location.find(locator)) == 0:
156157
continue
157158

158-
if loc.name == "Home":
159-
self._home_loc_id = loc.loc_id
160-
# Replace location-name for P1 legacy, can contain privacy-related info
161-
if self.smile.type == "power":
162-
loc.name = "Home"
159+
if loc._type == "building":
163160
self._home_loc_id = loc.loc_id
164161

165162
self._loc_data[loc.loc_id] = {"name": loc.name}

plugwise/smile.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from typing import Any, cast
1111

1212
from plugwise.constants import (
13-
ADAM,
1413
ALLOWED_ZONE_PROFILES,
1514
ANNA,
1615
APPLIANCES,
@@ -107,13 +106,12 @@ def get_all_gateway_entities(self) -> None:
107106
Collect and add switching- and/or pump-group entities.
108107
Finally, collect the data and states for each entity.
109108
"""
110-
self._all_appliances()
109+
self._get_appliances()
111110
if self._is_thermostat:
112111
self.therms_with_offset_func = (
113112
self._get_appliances_with_offset_functionality()
114113
)
115-
if self.check_name(ADAM):
116-
self._scan_thermostats()
114+
self._scan_thermostats()
117115

118116
if group_data := self._get_groups():
119117
self.gw_entities.update(group_data)

0 commit comments

Comments
 (0)