plugwise 1.7.6__tar.gz → 1.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {plugwise-1.7.6 → plugwise-1.7.7}/PKG-INFO +2 -1
  2. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/__init__.py +43 -51
  3. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/common.py +15 -5
  4. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/constants.py +1 -1
  5. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/data.py +12 -12
  6. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/helper.py +19 -36
  7. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/legacy/helper.py +15 -30
  8. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/legacy/smile.py +3 -18
  9. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/smile.py +27 -34
  10. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise.egg-info/PKG-INFO +2 -1
  11. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise.egg-info/requires.txt +1 -0
  12. {plugwise-1.7.6 → plugwise-1.7.7}/pyproject.toml +2 -1
  13. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_adam.py +16 -16
  14. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_anna.py +31 -31
  15. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_init.py +75 -136
  16. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_legacy_anna.py +4 -4
  17. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_legacy_p1.py +5 -5
  18. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_legacy_stretch.py +7 -7
  19. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_p1.py +5 -5
  20. {plugwise-1.7.6 → plugwise-1.7.7}/LICENSE +0 -0
  21. {plugwise-1.7.6 → plugwise-1.7.7}/README.md +0 -0
  22. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/exceptions.py +0 -0
  23. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/legacy/data.py +0 -0
  24. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/py.typed +0 -0
  25. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/smilecomm.py +0 -0
  26. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise/util.py +0 -0
  27. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise.egg-info/SOURCES.txt +0 -0
  28. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise.egg-info/dependency_links.txt +0 -0
  29. {plugwise-1.7.6 → plugwise-1.7.7}/plugwise.egg-info/top_level.txt +0 -0
  30. {plugwise-1.7.6 → plugwise-1.7.7}/setup.cfg +0 -0
  31. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_generic.py +0 -0
  32. {plugwise-1.7.6 → plugwise-1.7.7}/tests/test_legacy_generic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugwise
3
- Version: 1.7.6
3
+ Version: 1.7.7
4
4
  Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
5
5
  Author: Plugwise device owners
6
6
  Maintainer: bouwew, CoMPaTech
@@ -16,6 +16,7 @@ Classifier: Topic :: Home Automation
16
16
  Requires-Python: >=3.13
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
+ Requires-Dist: aiofiles
19
20
  Requires-Dist: aiohttp
20
21
  Requires-Dist: defusedxml
21
22
  Requires-Dist: munch
@@ -5,6 +5,8 @@ Plugwise backend module for Home Assistant Core.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from typing import cast
9
+
8
10
  from plugwise.constants import (
9
11
  DEFAULT_LEGACY_TIMEOUT,
10
12
  DEFAULT_PORT,
@@ -36,6 +38,7 @@ from plugwise.smilecomm import SmileComm
36
38
 
37
39
  import aiohttp
38
40
  from defusedxml import ElementTree as etree
41
+ from munch import Munch
39
42
  from packaging.version import Version, parse
40
43
 
41
44
 
@@ -72,16 +75,17 @@ class Smile(SmileComm):
72
75
  self._smile_api: SmileAPI | SmileLegacyAPI
73
76
  self._stretch_v2 = False
74
77
  self._target_smile: str = NONE
75
- self.smile_hostname: str = NONE
76
- self.smile_hw_version: str | None = None
77
- self.smile_legacy = False
78
- self.smile_mac_address: str | None = None
79
- self.smile_model: str = NONE
80
- self.smile_model_id: str | None = None
81
- self.smile_name: str = NONE
82
- self.smile_type: str = NONE
83
- self.smile_version: Version = Version("0.0.0")
84
- self.smile_zigbee_mac_address: str | None = None
78
+ self.smile: Munch = Munch()
79
+ self.smile.hostname = NONE
80
+ self.smile.hw_version = None
81
+ self.smile.legacy = False
82
+ self.smile.mac_address = None
83
+ self.smile.model = NONE
84
+ self.smile.model_id = None
85
+ self.smile.name = NONE
86
+ self.smile.type = NONE
87
+ self.smile.version = Version("0.0.0")
88
+ self.smile.zigbee_mac_address = None
85
89
 
86
90
  @property
87
91
  def cooling_present(self) -> bool:
@@ -109,7 +113,7 @@ class Smile(SmileComm):
109
113
 
110
114
  All non-legacy devices support gateway-rebooting.
111
115
  """
112
- return not self.smile_legacy
116
+ return not self.smile.legacy
113
117
 
114
118
  async def connect(self) -> Version:
115
119
  """Connect to the Plugwise Gateway and determine its name, type, version, and other data."""
@@ -158,16 +162,9 @@ class Smile(SmileComm):
158
162
  self._opentherm_device,
159
163
  self._request,
160
164
  self._schedule_old_states,
161
- self.smile_hostname,
162
- self.smile_hw_version,
163
- self.smile_mac_address,
164
- self.smile_model,
165
- self.smile_model_id,
166
- self.smile_name,
167
- self.smile_type,
168
- self.smile_version,
165
+ self.smile,
169
166
  )
170
- if not self.smile_legacy
167
+ if not self.smile.legacy
171
168
  else SmileLegacyAPI(
172
169
  self._is_thermostat,
173
170
  self._loc_data,
@@ -176,21 +173,14 @@ class Smile(SmileComm):
176
173
  self._request,
177
174
  self._stretch_v2,
178
175
  self._target_smile,
179
- self.smile_hostname,
180
- self.smile_hw_version,
181
- self.smile_mac_address,
182
- self.smile_model,
183
- self.smile_name,
184
- self.smile_type,
185
- self.smile_version,
186
- self.smile_zigbee_mac_address,
176
+ self.smile,
187
177
  )
188
178
  )
189
179
 
190
180
  # Update all endpoints on first connect
191
181
  await self._smile_api.full_xml_update()
192
182
 
193
- return self.smile_version
183
+ return cast(Version, self.smile.version)
194
184
 
195
185
  async def _smile_detect(
196
186
  self, result: etree.Element, dsmrmain: etree.Element
@@ -203,15 +193,17 @@ class Smile(SmileComm):
203
193
  if (gateway := result.find("./gateway")) is not None:
204
194
  if (v_model := gateway.find("vendor_model")) is not None:
205
195
  model = v_model.text
206
- self.smile_version = parse(gateway.find("firmware_version").text)
207
- self.smile_hw_version = gateway.find("hardware_version").text
208
- self.smile_hostname = gateway.find("hostname").text
209
- self.smile_mac_address = gateway.find("mac_address").text
210
- self.smile_model_id = gateway.find("vendor_model").text
196
+ self.smile.version = parse(gateway.find("firmware_version").text)
197
+ self.smile.hw_version = gateway.find("hardware_version").text
198
+ self.smile.hostname = gateway.find("hostname").text
199
+ self.smile.mac_address = gateway.find("mac_address").text
200
+ self.smile.model_id = gateway.find("vendor_model").text
211
201
  else:
212
202
  model = await self._smile_detect_legacy(result, dsmrmain, model)
213
203
 
214
- if model == "Unknown" or self.smile_version is None: # pragma: no cover
204
+ if model == "Unknown" or self.smile.version == Version(
205
+ "0.0.0"
206
+ ): # pragma: no cover
215
207
  # Corner case check
216
208
  LOGGER.error(
217
209
  "Unable to find model or version information, please create"
@@ -219,7 +211,7 @@ class Smile(SmileComm):
219
211
  )
220
212
  raise UnsupportedDeviceError
221
213
 
222
- version_major = str(self.smile_version.major)
214
+ version_major = str(self.smile.version.major)
223
215
  self._target_smile = f"{model}_v{version_major}"
224
216
  LOGGER.debug("Plugwise identified as %s", self._target_smile)
225
217
  if self._target_smile not in SMILES:
@@ -230,7 +222,7 @@ class Smile(SmileComm):
230
222
  )
231
223
  raise UnsupportedDeviceError
232
224
 
233
- if not self.smile_legacy:
225
+ if not self.smile.legacy:
234
226
  self._timeout = DEFAULT_TIMEOUT
235
227
 
236
228
  if self._target_smile in ("smile_open_therm_v2", "smile_thermo_v3"):
@@ -240,14 +232,14 @@ class Smile(SmileComm):
240
232
  ) # pragma: no cover
241
233
  raise UnsupportedDeviceError # pragma: no cover
242
234
 
243
- self.smile_model = "Gateway"
244
- self.smile_name = SMILES[self._target_smile].smile_name
245
- self.smile_type = SMILES[self._target_smile].smile_type
235
+ self.smile.model = "Gateway"
236
+ self.smile.name = SMILES[self._target_smile].smile_name
237
+ self.smile.type = SMILES[self._target_smile].smile_type
246
238
 
247
- if self.smile_type == "stretch":
239
+ if self.smile.type == "stretch":
248
240
  self._stretch_v2 = int(version_major) == 2
249
241
 
250
- if self.smile_type == "thermostat":
242
+ if self.smile.type == "thermostat":
251
243
  self._is_thermostat = True
252
244
  # For Adam, Anna, determine the system capabilities:
253
245
  # Find the connected heating/cooling device (heater_central),
@@ -275,13 +267,13 @@ class Smile(SmileComm):
275
267
  return_model = model
276
268
  # Stretch: find the MAC of the zigbee master_controller (= Stick)
277
269
  if (network := result.find("./module/protocols/master_controller")) is not None:
278
- self.smile_zigbee_mac_address = network.find("mac_address").text
270
+ self.smile.zigbee_mac_address = network.find("mac_address").text
279
271
  # Find the active MAC in case there is an orphaned Stick
280
272
  if zb_networks := result.findall("./network"):
281
273
  for zb_network in zb_networks:
282
274
  if zb_network.find("./nodes/network_router") is not None:
283
275
  network = zb_network.find("./master_controller")
284
- self.smile_zigbee_mac_address = network.find("mac_address").text
276
+ self.smile.zigbee_mac_address = network.find("mac_address").text
285
277
 
286
278
  # Legacy Anna or Stretch:
287
279
  if (
@@ -289,22 +281,22 @@ class Smile(SmileComm):
289
281
  or network is not None
290
282
  ):
291
283
  system = await self._request(SYSTEM)
292
- self.smile_version = parse(system.find("./gateway/firmware").text)
284
+ self.smile.version = parse(system.find("./gateway/firmware").text)
293
285
  return_model = str(system.find("./gateway/product").text)
294
- self.smile_hostname = system.find("./gateway/hostname").text
286
+ self.smile.hostname = system.find("./gateway/hostname").text
295
287
  # If wlan0 contains data it's active, eth0 should be checked last as is preferred
296
288
  for network in ("wlan0", "eth0"):
297
289
  locator = f"./{network}/mac"
298
290
  if (net_locator := system.find(locator)) is not None:
299
- self.smile_mac_address = net_locator.text
291
+ self.smile.mac_address = net_locator.text
300
292
 
301
293
  # P1 legacy:
302
294
  elif dsmrmain is not None:
303
295
  status = await self._request(STATUS)
304
- self.smile_version = parse(status.find("./system/version").text)
296
+ self.smile.version = parse(status.find("./system/version").text)
305
297
  return_model = str(status.find("./system/product").text)
306
- self.smile_hostname = status.find("./network/hostname").text
307
- self.smile_mac_address = status.find("./network/mac_address").text
298
+ self.smile.hostname = status.find("./network/hostname").text
299
+ self.smile.mac_address = status.find("./network/mac_address").text
308
300
  else: # pragma: no cover
309
301
  # No cornercase, just end of the line
310
302
  LOGGER.error(
@@ -313,7 +305,7 @@ class Smile(SmileComm):
313
305
  )
314
306
  raise ResponseError
315
307
 
316
- self.smile_legacy = True
308
+ self.smile.legacy = True
317
309
  return return_model
318
310
 
319
311
  async def async_update(self) -> dict[str, GwEntityData]:
@@ -10,6 +10,7 @@ from typing import cast
10
10
  from plugwise.constants import (
11
11
  ANNA,
12
12
  NONE,
13
+ PRIORITY_DEVICE_CLASSES,
13
14
  SPECIAL_PLUG_TYPES,
14
15
  SWITCH_GROUP_TYPES,
15
16
  ApplianceType,
@@ -55,17 +56,16 @@ class SmileCommon:
55
56
  self._heater_id: str = NONE
56
57
  self._on_off_device: bool
57
58
  self.gw_entities: dict[str, GwEntityData] = {}
58
- self.smile_name: str
59
- self.smile_type: str
59
+ self.smile: Munch
60
60
 
61
61
  @property
62
62
  def heater_id(self) -> str:
63
63
  """Return the heater-id."""
64
64
  return self._heater_id
65
65
 
66
- def smile(self, name: str) -> bool:
66
+ def check_name(self, name: str) -> bool:
67
67
  """Helper-function checking the smile-name."""
68
- return self.smile_name == name
68
+ return bool(self.smile.name == name)
69
69
 
70
70
  def _appl_heater_central_info(
71
71
  self,
@@ -153,6 +153,16 @@ class SmileCommon:
153
153
  self.gw_entities[appl.entity_id][appl_key] = value
154
154
  self._count += 1
155
155
 
156
+ def _reorder_devices(self) -> None:
157
+ """Place the gateway and optional heater_central devices as 1st and 2nd."""
158
+ reordered = {}
159
+ for dev_class in PRIORITY_DEVICE_CLASSES:
160
+ for entity_id, entity in dict(self.gw_entities).items():
161
+ if entity["dev_class"] == dev_class:
162
+ reordered[entity_id] = self.gw_entities.pop(entity_id)
163
+ break
164
+ self.gw_entities = {**reordered, **self.gw_entities}
165
+
156
166
  def _entity_switching_group(self, entity: GwEntityData, data: GwEntityData) -> None:
157
167
  """Helper-function for _get_device_zone_data().
158
168
 
@@ -173,7 +183,7 @@ class SmileCommon:
173
183
  """
174
184
  switch_groups: dict[str, GwEntityData] = {}
175
185
  # P1 and Anna don't have switchgroups
176
- if self.smile_type == "power" or self.smile(ANNA):
186
+ if self.smile.type == "power" or self.check_name(ANNA):
177
187
  return switch_groups
178
188
 
179
189
  for group in self._domain_objects.findall("./group"):
@@ -86,7 +86,7 @@ MIN_SETPOINT: Final[float] = 4.0
86
86
  MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
87
87
  NONE: Final = "None"
88
88
  OFF: Final = "off"
89
- PRIORITY_DEVICE_CLASSES = ("heater_central", "gateway")
89
+ PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central")
90
90
 
91
91
  # XML data paths
92
92
  APPLIANCES: Final = "/core/appliances"
@@ -35,7 +35,7 @@ class SmileData(SmileHelper):
35
35
  Collect data for each entity and add to self.gw_entities.
36
36
  """
37
37
  self._update_gw_entities()
38
- if self.smile(ADAM):
38
+ if self.check_name(ADAM):
39
39
  self._update_zones()
40
40
  self.gw_entities.update(self._zones)
41
41
 
@@ -86,7 +86,7 @@ class SmileData(SmileHelper):
86
86
  mac_pattern = re.compile(r"(?:[0-9A-F]{2}){8}")
87
87
  matches = ["Battery", "below"]
88
88
  if self._notifications:
89
- for msg_id, notification in list(self._notifications.items()):
89
+ for msg_id, notification in self._notifications.copy().items():
90
90
  mac_address: str | None = None
91
91
  message: str | None = notification.get("message")
92
92
  warning: str | None = notification.get("warning")
@@ -111,7 +111,7 @@ class SmileData(SmileHelper):
111
111
  """Helper-function adding or updating the Plugwise notifications."""
112
112
  if (
113
113
  entity_id == self._gateway_id
114
- and (self._is_thermostat or self.smile_type == "power")
114
+ and (self._is_thermostat or self.smile.type == "power")
115
115
  ) or (
116
116
  "binary_sensors" in entity
117
117
  and "plugwise_notification" in entity["binary_sensors"]
@@ -124,7 +124,7 @@ class SmileData(SmileHelper):
124
124
  """Helper-function for adding/updating various cooling-related values."""
125
125
  # For Anna and heating + cooling, replace setpoint with setpoint_high/_low
126
126
  if (
127
- self.smile(ANNA)
127
+ self.check_name(ANNA)
128
128
  and self._cooling_present
129
129
  and entity["dev_class"] == "thermostat"
130
130
  ):
@@ -194,11 +194,11 @@ class SmileData(SmileHelper):
194
194
  # Switching groups data
195
195
  self._entity_switching_group(entity, data)
196
196
  # Adam data
197
- if self.smile(ADAM):
197
+ if self.check_name(ADAM):
198
198
  self._get_adam_data(entity, data)
199
199
 
200
200
  # Thermostat data for Anna (presets, temperatures etc)
201
- if self.smile(ANNA) and entity["dev_class"] == "thermostat":
201
+ if self.check_name(ANNA) and entity["dev_class"] == "thermostat":
202
202
  self._climate_data(entity_id, entity, data)
203
203
  self._get_anna_control_state(data)
204
204
 
@@ -232,12 +232,12 @@ class SmileData(SmileHelper):
232
232
  if self._on_off_device and isinstance(self._heating_valves(), int):
233
233
  data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
234
234
  # Add cooling_enabled binary_sensor
235
- if "binary_sensors" in data:
236
- if (
237
- "cooling_enabled" not in data["binary_sensors"]
238
- and self._cooling_present
239
- ):
240
- data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
235
+ if (
236
+ "binary_sensors" in data
237
+ and "cooling_enabled" not in data["binary_sensors"]
238
+ and self._cooling_present
239
+ ):
240
+ data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
241
241
 
242
242
  # Show the allowed regulation_modes and gateway_modes
243
243
  if entity["dev_class"] == "gateway":
@@ -28,7 +28,6 @@ from plugwise.constants import (
28
28
  NONE,
29
29
  OFF,
30
30
  P1_MEASUREMENTS,
31
- PRIORITY_DEVICE_CLASSES,
32
31
  TEMP_CELSIUS,
33
32
  THERMOSTAT_CLASSES,
34
33
  TOGGLES,
@@ -85,11 +84,7 @@ class SmileHelper(SmileCommon):
85
84
  self._gateway_id: str = NONE
86
85
  self._zones: dict[str, GwEntityData]
87
86
  self.gw_entities: dict[str, GwEntityData]
88
- self.smile_hw_version: str | None
89
- self.smile_mac_address: str | None
90
- self.smile_model: str
91
- self.smile_model_id: str | None
92
- self.smile_version: version.Version
87
+ self.smile: Munch = Munch()
93
88
 
94
89
  @property
95
90
  def gateway_id(self) -> str:
@@ -160,11 +155,11 @@ class SmileHelper(SmileCommon):
160
155
 
161
156
  self._create_gw_entities(appl)
162
157
 
163
- if self.smile_type == "power":
158
+ if self.smile.type == "power":
164
159
  self._get_p1_smartmeter_info()
165
160
 
166
161
  # Sort the gw_entities
167
- self._sort_gw_entities()
162
+ self._reorder_devices()
168
163
 
169
164
  def _get_p1_smartmeter_info(self) -> None:
170
165
  """For P1 collect the connected SmartMeter info from the Home/building location.
@@ -197,18 +192,6 @@ class SmileHelper(SmileCommon):
197
192
 
198
193
  self._create_gw_entities(appl)
199
194
 
200
- def _sort_gw_entities(self) -> None:
201
- """Place the gateway and optional heater_central entities as 1st and 2nd."""
202
- for dev_class in PRIORITY_DEVICE_CLASSES:
203
- for entity_id, entity in dict(self.gw_entities).items():
204
- if entity["dev_class"] == dev_class:
205
- priority_entity = entity
206
- self.gw_entities.pop(entity_id)
207
- other_entities = self.gw_entities
208
- priority_entities = {entity_id: priority_entity}
209
- self.gw_entities = {**priority_entities, **other_entities}
210
- break
211
-
212
195
  def _all_locations(self) -> None:
213
196
  """Collect all locations."""
214
197
  loc = Munch()
@@ -268,16 +251,16 @@ class SmileHelper(SmileCommon):
268
251
  def _appl_gateway_info(self, appl: Munch, appliance: etree.Element) -> Munch:
269
252
  """Helper-function for _appliance_info_finder()."""
270
253
  self._gateway_id = appliance.attrib["id"]
271
- appl.firmware = str(self.smile_version)
272
- appl.hardware = self.smile_hw_version
273
- appl.mac = self.smile_mac_address
274
- appl.model = self.smile_model
275
- appl.model_id = self.smile_model_id
276
- appl.name = self.smile_name
254
+ appl.firmware = str(self.smile.version)
255
+ appl.hardware = self.smile.hw_version
256
+ appl.mac = self.smile.mac_address
257
+ appl.model = self.smile.model
258
+ appl.model_id = self.smile.model_id
259
+ appl.name = self.smile.name
277
260
  appl.vendor_name = "Plugwise"
278
261
 
279
262
  # Adam: collect the ZigBee MAC address of the Smile
280
- if self.smile(ADAM):
263
+ if self.check_name(ADAM):
281
264
  if (
282
265
  found := self._domain_objects.find(".//protocols/zig_bee_coordinator")
283
266
  ) is not None:
@@ -346,7 +329,7 @@ class SmileHelper(SmileCommon):
346
329
  # Get P1 smartmeter data from LOCATIONS
347
330
  entity = self.gw_entities[entity_id]
348
331
  # !! DON'T CHANGE below two if-lines, will break stuff !!
349
- if self.smile_type == "power":
332
+ if self.smile.type == "power":
350
333
  if entity["dev_class"] == "smartmeter":
351
334
  data.update(self._power_data_from_location())
352
335
 
@@ -383,7 +366,7 @@ class SmileHelper(SmileCommon):
383
366
  data.pop("c_heating_state")
384
367
  self._count -= 1
385
368
 
386
- if self._is_thermostat and self.smile(ANNA):
369
+ if self._is_thermostat and self.check_name(ANNA):
387
370
  self._update_anna_cooling(entity_id, data)
388
371
 
389
372
  self._cleanup_data(data)
@@ -484,7 +467,7 @@ class SmileHelper(SmileCommon):
484
467
  item == "thermostat"
485
468
  and (
486
469
  entity["dev_class"] != "climate"
487
- if self.smile(ADAM)
470
+ if self.check_name(ADAM)
488
471
  else entity["dev_class"] != "thermostat"
489
472
  )
490
473
  ):
@@ -539,7 +522,7 @@ class SmileHelper(SmileCommon):
539
522
 
540
523
  Collect the requested gateway mode.
541
524
  """
542
- if not (self.smile(ADAM) and entity_id == self._gateway_id):
525
+ if not (self.check_name(ADAM) and entity_id == self._gateway_id):
543
526
  return None
544
527
 
545
528
  if (search := search_actuator_functionalities(appliance, key)) is not None:
@@ -605,10 +588,10 @@ class SmileHelper(SmileCommon):
605
588
 
606
589
  Solution for Core issue #81839.
607
590
  """
608
- if self.smile(ANNA):
591
+ if self.check_name(ANNA):
609
592
  data["binary_sensors"]["heating_state"] = data["c_heating_state"]
610
593
 
611
- if self.smile(ADAM):
594
+ if self.check_name(ADAM):
612
595
  # First count when not present, then create and init to False.
613
596
  # When present init to False
614
597
  if "heating_state" not in data["binary_sensors"]:
@@ -723,7 +706,7 @@ class SmileHelper(SmileCommon):
723
706
  for entity_id, entity in self.gw_entities.items():
724
707
  self._rank_thermostat(thermo_matching, loc_id, entity_id, entity)
725
708
 
726
- for loc_id, loc_data in list(self._thermo_locs.items()):
709
+ for loc_id, loc_data in self._thermo_locs.items():
727
710
  if loc_data["primary_prio"] != 0:
728
711
  self._zones[loc_id] = {
729
712
  "dev_class": "climate",
@@ -799,8 +782,8 @@ class SmileHelper(SmileCommon):
799
782
 
800
783
  # Handle missing control_state in regulation_mode off for firmware >= 3.2.0 (issue #776)
801
784
  # In newer firmware versions, default to "off" when control_state is not present
802
- if self.smile_version != version.Version("0.0.0"):
803
- if self.smile_version >= version.parse("3.2.0"):
785
+ if self.smile.version != version.Version("0.0.0"):
786
+ if self.smile.version >= version.parse("3.2.0"):
804
787
  return "off"
805
788
 
806
789
  # Older Adam firmware does not have the control_state xml-key
@@ -23,7 +23,6 @@ from plugwise.constants import (
23
23
  NONE,
24
24
  OFF,
25
25
  P1_LEGACY_MEASUREMENTS,
26
- PRIORITY_DEVICE_CLASSES,
27
26
  TEMP_CELSIUS,
28
27
  THERMOSTAT_CLASSES,
29
28
  UOM,
@@ -47,7 +46,6 @@ from plugwise.util import (
47
46
  # This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
48
47
  from defusedxml import ElementTree as etree
49
48
  from munch import Munch
50
- from packaging.version import Version
51
49
 
52
50
 
53
51
  def etree_to_dict(element: etree.Element) -> dict[str, str]:
@@ -73,10 +71,7 @@ class SmileLegacyHelper(SmileCommon):
73
71
  self._modules: etree.Element
74
72
  self._stretch_v2: bool
75
73
  self.gw_entities: dict[str, GwEntityData] = {}
76
- self.smile_mac_address: str | None
77
- self.smile_model: str
78
- self.smile_version: Version
79
- self.smile_zigbee_mac_address: str | None
74
+ self.smile: Munch = Munch()
80
75
 
81
76
  @property
82
77
  def gateway_id(self) -> str:
@@ -95,7 +90,7 @@ class SmileLegacyHelper(SmileCommon):
95
90
 
96
91
  self._create_legacy_gateway()
97
92
  # For legacy P1 collect the connected SmartMeter info
98
- if self.smile_type == "power":
93
+ if self.smile.type == "power":
99
94
  appl = Munch()
100
95
  self._p1_smartmeter_info_finder(appl)
101
96
  # Legacy P1 has no more devices
@@ -140,17 +135,7 @@ class SmileLegacyHelper(SmileCommon):
140
135
  continue # pragma: no cover
141
136
 
142
137
  self._create_gw_entities(appl)
143
-
144
- # Place the gateway and optional heater_central devices as 1st and 2nd
145
- for dev_class in PRIORITY_DEVICE_CLASSES:
146
- for entity_id, entity in dict(self.gw_entities).items():
147
- if entity["dev_class"] == dev_class:
148
- tmp_entity = entity
149
- self.gw_entities.pop(entity_id)
150
- cleared_dict = self.gw_entities
151
- add_to_front = {entity_id: tmp_entity}
152
- self.gw_entities = {**add_to_front, **cleared_dict}
153
- break
138
+ self._reorder_devices()
154
139
 
155
140
  def _all_locations(self) -> None:
156
141
  """Collect all locations."""
@@ -167,13 +152,13 @@ class SmileLegacyHelper(SmileCommon):
167
152
  loc.loc_id = location.attrib["id"]
168
153
  # Filter the valid single location for P1 legacy: services not empty
169
154
  locator = "./services"
170
- if self.smile_type == "power" and len(location.find(locator)) == 0:
155
+ if self.smile.type == "power" and len(location.find(locator)) == 0:
171
156
  continue
172
157
 
173
158
  if loc.name == "Home":
174
159
  self._home_loc_id = loc.loc_id
175
160
  # Replace location-name for P1 legacy, can contain privacy-related info
176
- if self.smile_type == "power":
161
+ if self.smile.type == "power":
177
162
  loc.name = "Home"
178
163
  self._home_loc_id = loc.loc_id
179
164
 
@@ -185,18 +170,18 @@ class SmileLegacyHelper(SmileCommon):
185
170
  Use the home_location or FAKE_APPL as entity id.
186
171
  """
187
172
  self._gateway_id = self._home_loc_id
188
- if self.smile_type == "power":
173
+ if self.smile.type == "power":
189
174
  self._gateway_id = FAKE_APPL
190
175
 
191
176
  self.gw_entities[self._gateway_id] = {"dev_class": "gateway"}
192
177
  self._count += 1
193
178
  for key, value in {
194
- "firmware": str(self.smile_version),
179
+ "firmware": str(self.smile.version),
195
180
  "location": self._home_loc_id,
196
- "mac_address": self.smile_mac_address,
197
- "model": self.smile_model,
198
- "name": self.smile_name,
199
- "zigbee_mac_address": self.smile_zigbee_mac_address,
181
+ "mac_address": self.smile.mac_address,
182
+ "model": self.smile.model,
183
+ "name": self.smile.name,
184
+ "zigbee_mac_address": self.smile.zigbee_mac_address,
200
185
  "vendor": "Plugwise",
201
186
  }.items():
202
187
  if value is not None:
@@ -224,14 +209,14 @@ class SmileLegacyHelper(SmileCommon):
224
209
 
225
210
  Collect energy entity info (Smartmeter, Circle, Stealth, etc.): firmware, model and vendor name.
226
211
  """
227
- if self.smile_type in ("power", "stretch"):
212
+ if self.smile.type in ("power", "stretch"):
228
213
  locator = "./services/electricity_point_meter"
229
214
  module_data = self._get_module_data(
230
215
  appliance, locator, self._modules, legacy=True
231
216
  )
232
217
  appl.zigbee_mac = module_data["zigbee_mac_address"]
233
218
  # Filter appliance without zigbee_mac, it's an orphaned device
234
- if appl.zigbee_mac is None and self.smile_type != "power":
219
+ if appl.zigbee_mac is None and self.smile.type != "power":
235
220
  return None
236
221
 
237
222
  appl.hardware = module_data["hardware_version"]
@@ -253,7 +238,7 @@ class SmileLegacyHelper(SmileCommon):
253
238
  appl.entity_id = loc_id
254
239
  appl.location = loc_id
255
240
  appl.mac = None
256
- appl.model = self.smile_model
241
+ appl.model = self.smile.model
257
242
  appl.model_id = None
258
243
  appl.name = "P1"
259
244
  appl.pwclass = "smartmeter"
@@ -272,7 +257,7 @@ class SmileLegacyHelper(SmileCommon):
272
257
  # Get P1 smartmeter data from MODULES
273
258
  entity = self.gw_entities[entity_id]
274
259
  # !! DON'T CHANGE below two if-lines, will break stuff !!
275
- if self.smile_type == "power":
260
+ if self.smile.type == "power":
276
261
  if entity["dev_class"] == "smartmeter":
277
262
  data.update(self._power_data_from_modules())
278
263