|
30 | 30 | OFF, |
31 | 31 | P1_MEASUREMENTS, |
32 | 32 | TEMP_CELSIUS, |
| 33 | + THERMO_MATCHING, |
33 | 34 | THERMOSTAT_CLASSES, |
34 | 35 | TOGGLES, |
35 | 36 | UOM, |
|
56 | 57 | from packaging import version |
57 | 58 |
|
58 | 59 |
|
| 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 | + |
59 | 71 | def search_actuator_functionalities( |
60 | 72 | appliance: etree.Element, actuator: str |
61 | 73 | ) -> etree.Element | None: |
@@ -93,65 +105,59 @@ def item_count(self) -> int: |
93 | 105 | """Return the item-count.""" |
94 | 106 | return self._count |
95 | 107 |
|
96 | | - def _all_appliances(self) -> None: |
| 108 | + def _get_appliances(self) -> None: |
97 | 109 | """Collect all appliances with relevant info. |
98 | 110 |
|
99 | 111 | Also, collect the P1 smartmeter info from a location |
100 | 112 | as this one is not available as an appliance. |
101 | 113 | """ |
102 | 114 | self._count = 0 |
103 | | - self._all_locations() |
| 115 | + self._get_locations() |
104 | 116 |
|
105 | 117 | for appliance in self._domain_objects.findall("./appliance"): |
106 | 118 | 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 |
107 | 129 | 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 |
118 | 132 |
|
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 ( |
121 | 136 | appl.pwclass == "thermostat" |
122 | 137 | and appliance.find("actuator_functionalities/") is None |
123 | 138 | ): |
124 | 139 | continue |
125 | 140 |
|
126 | | - appl.location = None |
127 | 141 | if (appl_loc := appliance.find("location")) is not None: |
128 | 142 | 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 |
131 | 145 | elif appl.pwclass not in THERMOSTAT_CLASSES: |
132 | 146 | appl.location = self._home_loc_id |
133 | 147 |
|
134 | 148 | # Don't show orphaned thermostat-types |
135 | 149 | if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None: |
136 | 150 | continue |
137 | 151 |
|
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) |
148 | 153 |
|
149 | 154 | # Collect appliance info, skip orphaned/removed devices |
150 | 155 | if not (appl := self._appliance_info_finder(appl, appliance)): |
151 | 156 | continue |
152 | 157 |
|
153 | 158 | self._create_gw_entities(appl) |
154 | 159 |
|
| 160 | + # A smartmeter is not present as an appliance, add it specifically |
155 | 161 | if self.smile.type == "power" or self.smile.anna_p1: |
156 | 162 | self._get_p1_smartmeter_info() |
157 | 163 |
|
@@ -194,21 +200,33 @@ def _get_p1_smartmeter_info(self) -> None: |
194 | 200 |
|
195 | 201 | self._create_gw_entities(appl) |
196 | 202 |
|
197 | | - def _all_locations(self) -> None: |
| 203 | + def _get_locations(self) -> None: |
198 | 204 | """Collect all locations.""" |
| 205 | + counter = 0 |
199 | 206 | loc = Munch() |
200 | 207 | locations = self._domain_objects.findall("./location") |
201 | 208 | for location in locations: |
202 | | - loc.name = location.find("name").text |
203 | 209 | 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 | + ) |
207 | 225 |
|
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 |
212 | 230 |
|
213 | 231 | def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch: |
214 | 232 | """Collect info for all appliances found.""" |
@@ -739,84 +757,71 @@ def _cleanup_data(self, data: GwEntityData) -> None: |
739 | 757 | def _scan_thermostats(self) -> None: |
740 | 758 | """Helper-function for smile.py: get_all_entities(). |
741 | 759 |
|
742 | | - Update locations with thermostat ranking results and use |
| 760 | + Adam only: update locations with thermostat ranking results and use |
743 | 761 | the result to update the device_class of secondary thermostats. |
744 | 762 | """ |
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 |
757 | 765 |
|
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] = { |
761 | 770 | "dev_class": "climate", |
762 | 771 | "model": "ThermoZone", |
763 | | - "name": loc_data["name"], |
| 772 | + "name": location["name"], |
764 | 773 | "thermostats": { |
765 | | - "primary": loc_data["primary"], |
766 | | - "secondary": loc_data["secondary"], |
| 774 | + "primary": location["primary"], |
| 775 | + "secondary": location["secondary"], |
767 | 776 | }, |
768 | 777 | "vendor": "Plugwise", |
769 | 778 | } |
770 | 779 | self._count += 5 |
771 | 780 |
|
772 | | - def _match_locations(self) -> dict[str, ThermoLoc]: |
| 781 | + def _match_and_rank_thermostats(self) -> None: |
773 | 782 | """Helper-function for _scan_thermostats(). |
774 | 783 |
|
775 | | - Match appliances with locations. |
| 784 | + Match thermostat-appliances with locations, rank them for locations with multiple thermostats. |
776 | 785 | """ |
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 | + ) |
787 | 791 |
|
788 | 792 | def _rank_thermostat( |
789 | 793 | self, |
| 794 | + entity_id: str, |
| 795 | + entity: GwEntityData, |
| 796 | + location_id: str, |
| 797 | + location: ThermoLoc, |
790 | 798 | thermo_matching: dict[str, int], |
791 | | - loc_id: str, |
792 | | - appliance_id: str, |
793 | | - appliance_details: GwEntityData, |
794 | 799 | ) -> None: |
795 | 800 | """Helper-function for _scan_thermostats(). |
796 | 801 |
|
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. |
799 | 804 | """ |
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) |
820 | 825 |
|
821 | 826 | def _control_state(self, data: GwEntityData) -> str | bool: |
822 | 827 | """Helper-function for _get_location_data(). |
|
0 commit comments