plugwise 1.7.6__py3-none-any.whl → 1.7.8__py3-none-any.whl

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.
plugwise/__init__.py CHANGED
@@ -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]:
plugwise/common.py CHANGED
@@ -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"):
plugwise/constants.py CHANGED
@@ -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"
@@ -553,7 +553,7 @@ class GwEntityData(TypedDict, total=False):
553
553
  preset_modes: list[str] | None
554
554
  # Schedules:
555
555
  available_schedules: list[str]
556
- select_schedule: str
556
+ select_schedule: str | None
557
557
 
558
558
  climate_mode: str
559
559
  # Extra for Adam Master Thermostats
plugwise/data.py CHANGED
@@ -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":
@@ -268,11 +268,13 @@ class SmileData(SmileHelper):
268
268
  data["active_preset"] = self._preset(loc_id)
269
269
 
270
270
  # Schedule
271
+ data["available_schedules"] = []
272
+ data["select_schedule"] = None
273
+ self._count += 2
271
274
  avail_schedules, sel_schedule = self._schedules(loc_id)
272
275
  if avail_schedules != [NONE]:
273
276
  data["available_schedules"] = avail_schedules
274
277
  data["select_schedule"] = sel_schedule
275
- self._count += 2
276
278
 
277
279
  # Set HA climate HVACMode: auto, heat, heat_cool, cool and off
278
280
  data["climate_mode"] = "auto"
plugwise/helper.py CHANGED
@@ -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)
@@ -428,7 +411,8 @@ class SmileHelper(SmileCommon):
428
411
  case "elga_status_code":
429
412
  data["elga_status_code"] = int(appl_p_loc.text)
430
413
  case "select_dhw_mode":
431
- data["select_dhw_mode"] = appl_p_loc.text
414
+ if self._dhw_allowed_modes:
415
+ data["select_dhw_mode"] = appl_p_loc.text
432
416
 
433
417
  common_match_cases(measurement, attrs, appl_p_loc, data)
434
418
 
@@ -484,7 +468,7 @@ class SmileHelper(SmileCommon):
484
468
  item == "thermostat"
485
469
  and (
486
470
  entity["dev_class"] != "climate"
487
- if self.smile(ADAM)
471
+ if self.check_name(ADAM)
488
472
  else entity["dev_class"] != "thermostat"
489
473
  )
490
474
  ):
@@ -539,7 +523,7 @@ class SmileHelper(SmileCommon):
539
523
 
540
524
  Collect the requested gateway mode.
541
525
  """
542
- if not (self.smile(ADAM) and entity_id == self._gateway_id):
526
+ if not (entity_id == self._gateway_id and self.check_name(ADAM)):
543
527
  return None
544
528
 
545
529
  if (search := search_actuator_functionalities(appliance, key)) is not None:
@@ -552,29 +536,31 @@ class SmileHelper(SmileCommon):
552
536
  ) -> None:
553
537
  """Helper-function for _get_measurement_data().
554
538
 
555
- Adam: collect the gateway regulation_mode.
539
+ Adam gateway: collect the gateway regulation_mode.
556
540
  """
557
541
  if (
558
542
  mode := self._get_actuator_mode(
559
543
  appliance, entity_id, "regulation_mode_control_functionality"
560
544
  )
561
545
  ) is not None:
562
- data["select_regulation_mode"] = mode
563
- self._count += 1
546
+ # Below line needs to be here to set the boolean for both older and recent Adam firmware versions
564
547
  self._cooling_enabled = mode == "cooling"
548
+ if self._reg_allowed_modes:
549
+ data["select_regulation_mode"] = mode
550
+ self._count += 1
565
551
 
566
552
  def _get_gateway_mode(
567
553
  self, appliance: etree.Element, entity_id: str, data: GwEntityData
568
554
  ) -> None:
569
555
  """Helper-function for _get_measurement_data().
570
556
 
571
- Adam: collect the gateway mode.
557
+ Adam gateway: collect the gateway mode.
572
558
  """
573
559
  if (
574
560
  mode := self._get_actuator_mode(
575
561
  appliance, entity_id, "gateway_mode_control_functionality"
576
562
  )
577
- ) is not None:
563
+ ) is not None and self._gw_allowed_modes:
578
564
  data["select_gateway_mode"] = mode
579
565
  self._count += 1
580
566
 
@@ -605,10 +591,10 @@ class SmileHelper(SmileCommon):
605
591
 
606
592
  Solution for Core issue #81839.
607
593
  """
608
- if self.smile(ANNA):
594
+ if self.check_name(ANNA):
609
595
  data["binary_sensors"]["heating_state"] = data["c_heating_state"]
610
596
 
611
- if self.smile(ADAM):
597
+ if self.check_name(ADAM):
612
598
  # First count when not present, then create and init to False.
613
599
  # When present init to False
614
600
  if "heating_state" not in data["binary_sensors"]:
@@ -723,7 +709,7 @@ class SmileHelper(SmileCommon):
723
709
  for entity_id, entity in self.gw_entities.items():
724
710
  self._rank_thermostat(thermo_matching, loc_id, entity_id, entity)
725
711
 
726
- for loc_id, loc_data in list(self._thermo_locs.items()):
712
+ for loc_id, loc_data in self._thermo_locs.items():
727
713
  if loc_data["primary_prio"] != 0:
728
714
  self._zones[loc_id] = {
729
715
  "dev_class": "climate",
@@ -799,8 +785,8 @@ class SmileHelper(SmileCommon):
799
785
 
800
786
  # Handle missing control_state in regulation_mode off for firmware >= 3.2.0 (issue #776)
801
787
  # 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"):
788
+ if self.smile.version != version.Version("0.0.0"):
789
+ if self.smile.version >= version.parse("3.2.0"):
804
790
  return "off"
805
791
 
806
792
  # Older Adam firmware does not have the control_state xml-key
plugwise/legacy/data.py CHANGED
@@ -67,11 +67,13 @@ class SmileLegacyData(SmileLegacyHelper):
67
67
  data["active_preset"] = self._preset()
68
68
 
69
69
  # Schedule
70
+ data["available_schedules"] = []
71
+ data["select_schedule"] = None
72
+ self._count += 2
70
73
  avail_schedules, sel_schedule = self._schedules()
71
74
  if avail_schedules != [NONE]:
72
75
  data["available_schedules"] = avail_schedules
73
76
  data["select_schedule"] = sel_schedule
74
- self._count += 2
75
77
 
76
78
  # Set HA climate HVACMode: auto, heat
77
79
  data["climate_mode"] = "auto"
plugwise/legacy/helper.py CHANGED
@@ -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
 
plugwise/legacy/smile.py CHANGED
@@ -27,7 +27,6 @@ from plugwise.exceptions import ConnectionFailedError, DataMissingError, Plugwis
27
27
  from plugwise.legacy.data import SmileLegacyData
28
28
 
29
29
  from munch import Munch
30
- from packaging.version import Version
31
30
 
32
31
 
33
32
  class SmileLegacyAPI(SmileLegacyData):
@@ -44,14 +43,7 @@ class SmileLegacyAPI(SmileLegacyData):
44
43
  _request: Callable[..., Awaitable[Any]],
45
44
  _stretch_v2: bool,
46
45
  _target_smile: str,
47
- smile_hostname: str,
48
- smile_hw_version: str | None,
49
- smile_mac_address: str | None,
50
- smile_model: str,
51
- smile_name: str,
52
- smile_type: str,
53
- smile_version: Version,
54
- smile_zigbee_mac_address: str | None,
46
+ smile: Munch,
55
47
  ) -> None:
56
48
  """Set the constructor for this class."""
57
49
  super().__init__()
@@ -63,14 +55,7 @@ class SmileLegacyAPI(SmileLegacyData):
63
55
  self._request = _request
64
56
  self._stretch_v2 = _stretch_v2
65
57
  self._target_smile = _target_smile
66
- self.smile_hostname = smile_hostname
67
- self.smile_hw_version = smile_hw_version
68
- self.smile_mac_address = smile_mac_address
69
- self.smile_model = smile_model
70
- self.smile_name = smile_name
71
- self.smile_type = smile_type
72
- self.smile_version = smile_version
73
- self.smile_zigbee_mac_address = smile_zigbee_mac_address
58
+ self.smile = smile
74
59
 
75
60
  self._first_update = True
76
61
  self._previous_day_number: str = "0"
@@ -86,7 +71,7 @@ class SmileLegacyAPI(SmileLegacyData):
86
71
  self._locations = await self._request(LOCATIONS)
87
72
  self._modules = await self._request(MODULES)
88
73
  # P1 legacy has no appliances
89
- if self.smile_type != "power":
74
+ if self.smile.type != "power":
90
75
  self._appliances = await self._request(APPLIANCES)
91
76
 
92
77
  def get_all_gateway_entities(self) -> None:
plugwise/smile.py CHANGED
@@ -35,7 +35,27 @@ from defusedxml import ElementTree as etree
35
35
 
36
36
  # Dict as class
37
37
  from munch import Munch
38
- from packaging.version import Version
38
+
39
+
40
+ def model_to_switch_items(model: str, state: str, switch: Munch) -> tuple[str, Munch]:
41
+ """Translate state and switch attributes based on model name.
42
+
43
+ Helper function for set_switch_state().
44
+ """
45
+ match model:
46
+ case "dhw_cm_switch":
47
+ switch.device = "toggle"
48
+ switch.func_type = "toggle_functionality"
49
+ switch.act_type = "domestic_hot_water_comfort_mode"
50
+ case "cooling_ena_switch":
51
+ switch.device = "toggle"
52
+ switch.func_type = "toggle_functionality"
53
+ switch.act_type = "cooling_enabled"
54
+ case "lock":
55
+ switch.func = "lock"
56
+ state = "true" if state == STATE_ON else "false"
57
+
58
+ return state, switch
39
59
 
40
60
 
41
61
  class SmileAPI(SmileData):
@@ -54,14 +74,7 @@ class SmileAPI(SmileData):
54
74
  _opentherm_device: bool,
55
75
  _request: Callable[..., Awaitable[Any]],
56
76
  _schedule_old_states: dict[str, dict[str, str]],
57
- smile_hostname: str | None,
58
- smile_hw_version: str | None,
59
- smile_mac_address: str | None,
60
- smile_model: str,
61
- smile_model_id: str | None,
62
- smile_name: str,
63
- smile_type: str,
64
- smile_version: Version,
77
+ smile: Munch,
65
78
  ) -> None:
66
79
  """Set the constructor for this class."""
67
80
  super().__init__()
@@ -74,14 +87,7 @@ class SmileAPI(SmileData):
74
87
  self._opentherm_device = _opentherm_device
75
88
  self._request = _request
76
89
  self._schedule_old_states = _schedule_old_states
77
- self.smile_hostname = smile_hostname
78
- self.smile_hw_version = smile_hw_version
79
- self.smile_mac_address = smile_mac_address
80
- self.smile_model = smile_model
81
- self.smile_model_id = smile_model_id
82
- self.smile_name = smile_name
83
- self.smile_type = smile_type
84
- self.smile_version = smile_version
90
+ self.smile = smile
85
91
  self.therms_with_offset_func: list[str] = []
86
92
 
87
93
  @property
@@ -107,7 +113,7 @@ class SmileAPI(SmileData):
107
113
  self.therms_with_offset_func = (
108
114
  self._get_appliances_with_offset_functionality()
109
115
  )
110
- if self.smile(ADAM):
116
+ if self.check_name(ADAM):
111
117
  self._scan_thermostats()
112
118
 
113
119
  if group_data := self._get_group_switches():
@@ -340,7 +346,7 @@ class SmileAPI(SmileData):
340
346
  template = (
341
347
  '<template tag="zone_preset_based_on_time_and_presence_with_override" />'
342
348
  )
343
- if self.smile(ANNA):
349
+ if self.check_name(ANNA):
344
350
  locator = f'.//*[@id="{schedule_rule_id}"]/template'
345
351
  template_id = self._domain_objects.find(locator).attrib["id"]
346
352
  template = f'<template id="{template_id}" />'
@@ -396,20 +402,7 @@ class SmileAPI(SmileData):
396
402
  switch.device = "relay"
397
403
  switch.func_type = "relay_functionality"
398
404
  switch.func = "state"
399
- if model == "dhw_cm_switch":
400
- switch.device = "toggle"
401
- switch.func_type = "toggle_functionality"
402
- switch.act_type = "domestic_hot_water_comfort_mode"
403
-
404
- if model == "cooling_ena_switch":
405
- switch.device = "toggle"
406
- switch.func_type = "toggle_functionality"
407
- switch.act_type = "cooling_enabled"
408
-
409
- if model == "lock":
410
- switch.func = "lock"
411
- state = "true" if state == STATE_ON else "false"
412
-
405
+ state, switch = model_to_switch_items(model, state, switch)
413
406
  data = (
414
407
  f"<{switch.func_type}>"
415
408
  f"<{switch.func}>{state}</{switch.func}>"
@@ -476,7 +469,7 @@ class SmileAPI(SmileData):
476
469
  if "setpoint" in items:
477
470
  setpoint = items["setpoint"]
478
471
 
479
- if self.smile(ANNA) and self._cooling_present:
472
+ if self.check_name(ANNA) and self._cooling_present:
480
473
  if "setpoint_high" not in items:
481
474
  raise PlugwiseError(
482
475
  "Plugwise: failed setting temperature: no valid input provided"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugwise
3
- Version: 1.7.6
3
+ Version: 1.7.8
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
@@ -0,0 +1,18 @@
1
+ plugwise/__init__.py,sha256=tctQhmRTvr70Am9x5V1UZf_VtPlw6o2if1MlgMVGcYo,17408
2
+ plugwise/common.py,sha256=vP4DgD1_Sv5B-7MwSjiYn7xoke20oDWggQ3aY5W1Jeg,10084
3
+ plugwise/constants.py,sha256=VMpZcH5l9KCsFTuft4bLrPGzRMveBrUu_R0iyjuZReg,16924
4
+ plugwise/data.py,sha256=vtOot1JrgE7rHdt1pSnrh0BNAetcsPlTnoRD3SRD5lc,12571
5
+ plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
6
+ plugwise/helper.py,sha256=zxLWcPThtlyCw5ICpYg3E2sBXIlR_074G9czBw31ZLA,39165
7
+ plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ plugwise/smile.py,sha256=hk_KTOzajekJvxi05g4sJoJ49YvlSLLUiNluvMUbKBM,20319
9
+ plugwise/smilecomm.py,sha256=DRQ3toRNEf3oo_mej49fPJ47m5das-jvo-8GnIrSPzw,5208
10
+ plugwise/util.py,sha256=rMcqfaB4dkQEZFJY-bBJISmlYgTnb6Ns3-Doxelf92Q,10689
11
+ plugwise/legacy/data.py,sha256=ICZRK4pI75RtAfNilrUkmv3tEmd6cEZt66mI0a_V7-E,3221
12
+ plugwise/legacy/helper.py,sha256=vUuYX-vJErbVwGJu7On1ITDh8X8nzO8WoqN-whS9FJ4,16710
13
+ plugwise/legacy/smile.py,sha256=RBfasiOrAXUWwvUqciO0xzel4VMNsY0Nzb44GHqDPpo,12837
14
+ plugwise-1.7.8.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
15
+ plugwise-1.7.8.dist-info/METADATA,sha256=Hw_-01UrDvbsU8mUxgG8z9YEqNgDjgUH8Xm1c9CgNLE,7903
16
+ plugwise-1.7.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ plugwise-1.7.8.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
18
+ plugwise-1.7.8.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- plugwise/__init__.py,sha256=yLUc573EjJTvT4ZyLKbILPYgKpPZ9oqR4DQYev-7UWI,17883
2
- plugwise/common.py,sha256=_O7cC7fPGHZkrxQTaK2y8_trr9lOjEtgbS6plOfp2jk,9589
3
- plugwise/constants.py,sha256=eivym_wEqoysjPBDKG-9gbrUdG3MuCLxxx3KEHjDO7I,16917
4
- plugwise/data.py,sha256=OxZufaAmnyVDkRGJBdk6tf-I65FaCwcT69scNsZnmiA,12490
5
- plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
6
- plugwise/helper.py,sha256=AdQ4Tno5zheFI5y5A-YovVGYW_islxCxkt0B77TfC-o,39701
7
- plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- plugwise/smile.py,sha256=lHlaqvGAVbyy3Qnds1U_pv1piJutF5_a6P8ZdgGSvHA,20601
9
- plugwise/smilecomm.py,sha256=DRQ3toRNEf3oo_mej49fPJ47m5das-jvo-8GnIrSPzw,5208
10
- plugwise/util.py,sha256=rMcqfaB4dkQEZFJY-bBJISmlYgTnb6Ns3-Doxelf92Q,10689
11
- plugwise/legacy/data.py,sha256=Z-7nw21s9-L4DcwPCZ_yoRGeI_fWBS1Q48kHmyh2pkY,3145
12
- plugwise/legacy/helper.py,sha256=cDi8zvUtoCqxVrLksqjlCHUmjXjulXCf-l9fMjFPOfs,17417
13
- plugwise/legacy/smile.py,sha256=2GyKn0yAcg-GdVHvyoe5LfPzs0WAlIRwHWc8bhRg6DQ,13452
14
- plugwise-1.7.6.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
15
- plugwise-1.7.6.dist-info/METADATA,sha256=XabqKhZBkpv6YfSQ52kWubwR1k-dapgFRZYvc3fiupY,7879
16
- plugwise-1.7.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- plugwise-1.7.6.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
18
- plugwise-1.7.6.dist-info/RECORD,,