plugwise 1.7.5__py3-none-any.whl → 1.7.7__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,
@@ -15,6 +17,8 @@ from plugwise.constants import (
15
17
  MODULES,
16
18
  NONE,
17
19
  SMILES,
20
+ STATE_OFF,
21
+ STATE_ON,
18
22
  STATUS,
19
23
  SYSTEM,
20
24
  GwEntityData,
@@ -34,6 +38,7 @@ from plugwise.smilecomm import SmileComm
34
38
 
35
39
  import aiohttp
36
40
  from defusedxml import ElementTree as etree
41
+ from munch import Munch
37
42
  from packaging.version import Version, parse
38
43
 
39
44
 
@@ -70,16 +75,17 @@ class Smile(SmileComm):
70
75
  self._smile_api: SmileAPI | SmileLegacyAPI
71
76
  self._stretch_v2 = False
72
77
  self._target_smile: str = NONE
73
- self.smile_hostname: str = NONE
74
- self.smile_hw_version: str | None = None
75
- self.smile_legacy = False
76
- self.smile_mac_address: str | None = None
77
- self.smile_model: str = NONE
78
- self.smile_model_id: str | None = None
79
- self.smile_name: str = NONE
80
- self.smile_type: str = NONE
81
- self.smile_version: Version = Version("0.0.0")
82
- 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
83
89
 
84
90
  @property
85
91
  def cooling_present(self) -> bool:
@@ -107,7 +113,7 @@ class Smile(SmileComm):
107
113
 
108
114
  All non-legacy devices support gateway-rebooting.
109
115
  """
110
- return not self.smile_legacy
116
+ return not self.smile.legacy
111
117
 
112
118
  async def connect(self) -> Version:
113
119
  """Connect to the Plugwise Gateway and determine its name, type, version, and other data."""
@@ -156,16 +162,9 @@ class Smile(SmileComm):
156
162
  self._opentherm_device,
157
163
  self._request,
158
164
  self._schedule_old_states,
159
- self.smile_hostname,
160
- self.smile_hw_version,
161
- self.smile_mac_address,
162
- self.smile_model,
163
- self.smile_model_id,
164
- self.smile_name,
165
- self.smile_type,
166
- self.smile_version,
165
+ self.smile,
167
166
  )
168
- if not self.smile_legacy
167
+ if not self.smile.legacy
169
168
  else SmileLegacyAPI(
170
169
  self._is_thermostat,
171
170
  self._loc_data,
@@ -174,21 +173,14 @@ class Smile(SmileComm):
174
173
  self._request,
175
174
  self._stretch_v2,
176
175
  self._target_smile,
177
- self.smile_hostname,
178
- self.smile_hw_version,
179
- self.smile_mac_address,
180
- self.smile_model,
181
- self.smile_name,
182
- self.smile_type,
183
- self.smile_version,
184
- self.smile_zigbee_mac_address,
176
+ self.smile,
185
177
  )
186
178
  )
187
179
 
188
180
  # Update all endpoints on first connect
189
181
  await self._smile_api.full_xml_update()
190
182
 
191
- return self.smile_version
183
+ return cast(Version, self.smile.version)
192
184
 
193
185
  async def _smile_detect(
194
186
  self, result: etree.Element, dsmrmain: etree.Element
@@ -201,15 +193,17 @@ class Smile(SmileComm):
201
193
  if (gateway := result.find("./gateway")) is not None:
202
194
  if (v_model := gateway.find("vendor_model")) is not None:
203
195
  model = v_model.text
204
- self.smile_version = parse(gateway.find("firmware_version").text)
205
- self.smile_hw_version = gateway.find("hardware_version").text
206
- self.smile_hostname = gateway.find("hostname").text
207
- self.smile_mac_address = gateway.find("mac_address").text
208
- 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
209
201
  else:
210
202
  model = await self._smile_detect_legacy(result, dsmrmain, model)
211
203
 
212
- 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
213
207
  # Corner case check
214
208
  LOGGER.error(
215
209
  "Unable to find model or version information, please create"
@@ -217,7 +211,7 @@ class Smile(SmileComm):
217
211
  )
218
212
  raise UnsupportedDeviceError
219
213
 
220
- version_major = str(self.smile_version.major)
214
+ version_major = str(self.smile.version.major)
221
215
  self._target_smile = f"{model}_v{version_major}"
222
216
  LOGGER.debug("Plugwise identified as %s", self._target_smile)
223
217
  if self._target_smile not in SMILES:
@@ -228,7 +222,7 @@ class Smile(SmileComm):
228
222
  )
229
223
  raise UnsupportedDeviceError
230
224
 
231
- if not self.smile_legacy:
225
+ if not self.smile.legacy:
232
226
  self._timeout = DEFAULT_TIMEOUT
233
227
 
234
228
  if self._target_smile in ("smile_open_therm_v2", "smile_thermo_v3"):
@@ -238,14 +232,14 @@ class Smile(SmileComm):
238
232
  ) # pragma: no cover
239
233
  raise UnsupportedDeviceError # pragma: no cover
240
234
 
241
- self.smile_model = "Gateway"
242
- self.smile_name = SMILES[self._target_smile].smile_name
243
- 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
244
238
 
245
- if self.smile_type == "stretch":
239
+ if self.smile.type == "stretch":
246
240
  self._stretch_v2 = int(version_major) == 2
247
241
 
248
- if self.smile_type == "thermostat":
242
+ if self.smile.type == "thermostat":
249
243
  self._is_thermostat = True
250
244
  # For Adam, Anna, determine the system capabilities:
251
245
  # Find the connected heating/cooling device (heater_central),
@@ -273,13 +267,13 @@ class Smile(SmileComm):
273
267
  return_model = model
274
268
  # Stretch: find the MAC of the zigbee master_controller (= Stick)
275
269
  if (network := result.find("./module/protocols/master_controller")) is not None:
276
- self.smile_zigbee_mac_address = network.find("mac_address").text
270
+ self.smile.zigbee_mac_address = network.find("mac_address").text
277
271
  # Find the active MAC in case there is an orphaned Stick
278
272
  if zb_networks := result.findall("./network"):
279
273
  for zb_network in zb_networks:
280
274
  if zb_network.find("./nodes/network_router") is not None:
281
275
  network = zb_network.find("./master_controller")
282
- self.smile_zigbee_mac_address = network.find("mac_address").text
276
+ self.smile.zigbee_mac_address = network.find("mac_address").text
283
277
 
284
278
  # Legacy Anna or Stretch:
285
279
  if (
@@ -287,22 +281,22 @@ class Smile(SmileComm):
287
281
  or network is not None
288
282
  ):
289
283
  system = await self._request(SYSTEM)
290
- self.smile_version = parse(system.find("./gateway/firmware").text)
284
+ self.smile.version = parse(system.find("./gateway/firmware").text)
291
285
  return_model = str(system.find("./gateway/product").text)
292
- self.smile_hostname = system.find("./gateway/hostname").text
286
+ self.smile.hostname = system.find("./gateway/hostname").text
293
287
  # If wlan0 contains data it's active, eth0 should be checked last as is preferred
294
288
  for network in ("wlan0", "eth0"):
295
289
  locator = f"./{network}/mac"
296
290
  if (net_locator := system.find(locator)) is not None:
297
- self.smile_mac_address = net_locator.text
291
+ self.smile.mac_address = net_locator.text
298
292
 
299
293
  # P1 legacy:
300
294
  elif dsmrmain is not None:
301
295
  status = await self._request(STATUS)
302
- self.smile_version = parse(status.find("./system/version").text)
296
+ self.smile.version = parse(status.find("./system/version").text)
303
297
  return_model = str(status.find("./system/product").text)
304
- self.smile_hostname = status.find("./network/hostname").text
305
- 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
306
300
  else: # pragma: no cover
307
301
  # No cornercase, just end of the line
308
302
  LOGGER.error(
@@ -311,7 +305,7 @@ class Smile(SmileComm):
311
305
  )
312
306
  raise ResponseError
313
307
 
314
- self.smile_legacy = True
308
+ self.smile.legacy = True
315
309
  return return_model
316
310
 
317
311
  async def async_update(self) -> dict[str, GwEntityData]:
@@ -398,10 +392,21 @@ class Smile(SmileComm):
398
392
 
399
393
  async def set_switch_state(
400
394
  self, appl_id: str, members: list[str] | None, model: str, state: str
401
- ) -> None:
402
- """Set the given State of the relevant Switch."""
395
+ ) -> bool:
396
+ """Set the given State of the relevant Switch.
397
+
398
+ Return the result:
399
+ - True when switched to state on,
400
+ - False when switched to state off,
401
+ - the unchanged state when the switch is for instance locked.
402
+ """
403
+ if state not in (STATE_OFF, STATE_ON):
404
+ raise PlugwiseError("Invalid state supplied to set_switch_state")
405
+
403
406
  try:
404
- await self._smile_api.set_switch_state(appl_id, members, model, state)
407
+ return await self._smile_api.set_switch_state(
408
+ appl_id, members, model, state
409
+ )
405
410
  except ConnectionFailedError as exc:
406
411
  raise ConnectionFailedError(
407
412
  f"Failed to set switch state: {str(exc)}"
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
@@ -23,6 +23,8 @@ POWER_WATT: Final = "W"
23
23
  PRESET_AWAY: Final = "away"
24
24
  PRESSURE_BAR: Final = "bar"
25
25
  SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
26
+ STATE_OFF: Final = "off"
27
+ STATE_ON: Final = "on"
26
28
  TEMP_CELSIUS: Final = "°C"
27
29
  TEMP_KELVIN: Final = "°K"
28
30
  TIME_MILLISECONDS: Final = "ms"
@@ -84,7 +86,7 @@ MIN_SETPOINT: Final[float] = 4.0
84
86
  MODULE_LOCATOR: Final = "./logs/point_log/*[@id]"
85
87
  NONE: Final = "None"
86
88
  OFF: Final = "off"
87
- PRIORITY_DEVICE_CLASSES = ("heater_central", "gateway")
89
+ PRIORITY_DEVICE_CLASSES = ("gateway", "heater_central")
88
90
 
89
91
  # XML data paths
90
92
  APPLIANCES: Final = "/core/appliances"
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":
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)
@@ -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
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
@@ -18,6 +18,8 @@ from plugwise.constants import (
18
18
  OFF,
19
19
  REQUIRE_APPLIANCES,
20
20
  RULES,
21
+ STATE_OFF,
22
+ STATE_ON,
21
23
  GwEntityData,
22
24
  ThermoLoc,
23
25
  )
@@ -25,7 +27,6 @@ from plugwise.exceptions import ConnectionFailedError, DataMissingError, Plugwis
25
27
  from plugwise.legacy.data import SmileLegacyData
26
28
 
27
29
  from munch import Munch
28
- from packaging.version import Version
29
30
 
30
31
 
31
32
  class SmileLegacyAPI(SmileLegacyData):
@@ -42,14 +43,7 @@ class SmileLegacyAPI(SmileLegacyData):
42
43
  _request: Callable[..., Awaitable[Any]],
43
44
  _stretch_v2: bool,
44
45
  _target_smile: str,
45
- smile_hostname: str,
46
- smile_hw_version: str | None,
47
- smile_mac_address: str | None,
48
- smile_model: str,
49
- smile_name: str,
50
- smile_type: str,
51
- smile_version: Version,
52
- smile_zigbee_mac_address: str | None,
46
+ smile: Munch,
53
47
  ) -> None:
54
48
  """Set the constructor for this class."""
55
49
  super().__init__()
@@ -61,14 +55,7 @@ class SmileLegacyAPI(SmileLegacyData):
61
55
  self._request = _request
62
56
  self._stretch_v2 = _stretch_v2
63
57
  self._target_smile = _target_smile
64
- self.smile_hostname = smile_hostname
65
- self.smile_hw_version = smile_hw_version
66
- self.smile_mac_address = smile_mac_address
67
- self.smile_model = smile_model
68
- self.smile_name = smile_name
69
- self.smile_type = smile_type
70
- self.smile_version = smile_version
71
- self.smile_zigbee_mac_address = smile_zigbee_mac_address
58
+ self.smile = smile
72
59
 
73
60
  self._first_update = True
74
61
  self._previous_day_number: str = "0"
@@ -84,7 +71,7 @@ class SmileLegacyAPI(SmileLegacyData):
84
71
  self._locations = await self._request(LOCATIONS)
85
72
  self._modules = await self._request(MODULES)
86
73
  # P1 legacy has no appliances
87
- if self.smile_type != "power":
74
+ if self.smile.type != "power":
88
75
  self._appliances = await self._request(APPLIANCES)
89
76
 
90
77
  def get_all_gateway_entities(self) -> None:
@@ -195,7 +182,7 @@ class SmileLegacyAPI(SmileLegacyData):
195
182
  Determined from - DOMAIN_OBJECTS.
196
183
  Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
197
184
  """
198
- if state not in ("on", "off"):
185
+ if state not in (STATE_OFF, STATE_ON):
199
186
  raise PlugwiseError("Plugwise: invalid schedule state.")
200
187
 
201
188
  # Handle no schedule-name / Off-schedule provided
@@ -214,7 +201,7 @@ class SmileLegacyAPI(SmileLegacyData):
214
201
  ) # pragma: no cover
215
202
 
216
203
  new_state = "false"
217
- if state == "on":
204
+ if state == STATE_ON:
218
205
  new_state = "true"
219
206
 
220
207
  locator = f'.//*[@id="{schedule_rule_id}"]/template'
@@ -234,13 +221,16 @@ class SmileLegacyAPI(SmileLegacyData):
234
221
 
235
222
  async def set_switch_state(
236
223
  self, appl_id: str, members: list[str] | None, model: str, state: str
237
- ) -> None:
224
+ ) -> bool:
238
225
  """Set the given state of the relevant switch.
239
226
 
240
227
  For individual switches, sets the state directly.
241
228
  For group switches, sets the state for each member in the group separately.
242
229
  For switch-locks, sets the lock state using a different data format.
230
+ Return the requested state when succesful, the current state otherwise.
243
231
  """
232
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
233
+ requested_state = state == STATE_ON
244
234
  switch = Munch()
245
235
  switch.actuator = "actuator_functionalities"
246
236
  switch.func_type = "relay_functionality"
@@ -250,7 +240,7 @@ class SmileLegacyAPI(SmileLegacyData):
250
240
 
251
241
  # Handle switch-lock
252
242
  if model == "lock":
253
- state = "false" if state == "off" else "true"
243
+ state = "true" if state == STATE_ON else "false"
254
244
  appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
255
245
  appl_name = appliance.find("name").text
256
246
  appl_type = appliance.find("type").text
@@ -269,37 +259,45 @@ class SmileLegacyAPI(SmileLegacyData):
269
259
  "</appliances>"
270
260
  )
271
261
  await self.call_request(APPLIANCES, method="post", data=data)
272
- return
262
+ return requested_state
273
263
 
274
264
  # Handle group of switches
275
265
  data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
276
266
  if members is not None:
277
267
  return await self._set_groupswitch_member_state(
278
- data, members, state, switch
268
+ appl_id, data, members, state, switch
279
269
  )
280
270
 
281
271
  # Handle individual relay switches
282
272
  uri = f"{APPLIANCES};id={appl_id}/relay"
283
- if model == "relay":
284
- locator = (
285
- f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
286
- )
273
+ if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]:
287
274
  # Don't bother switching a relay when the corresponding lock-state is true
288
- if self._appliances.find(locator).text == "true":
289
- raise PlugwiseError("Plugwise: the locked Relay was not switched.")
275
+ return current_state
290
276
 
291
277
  await self.call_request(uri, method="put", data=data)
278
+ return requested_state
292
279
 
293
280
  async def _set_groupswitch_member_state(
294
- self, data: str, members: list[str], state: str, switch: Munch
295
- ) -> None:
281
+ self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
282
+ ) -> bool:
296
283
  """Helper-function for set_switch_state().
297
284
 
298
- Set the given State of the relevant Switch (relay) within a group of members.
285
+ Set the requested state of the relevant switch within a group of switches.
286
+ Return the current group-state when none of the switches has changed its state, the requested state otherwise.
299
287
  """
288
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
289
+ requested_state = state == STATE_ON
290
+ switched = 0
300
291
  for member in members:
301
- uri = f"{APPLIANCES};id={member}/relay"
302
- await self.call_request(uri, method="put", data=data)
292
+ if not self.gw_entities[member]["switches"]["lock"]:
293
+ uri = f"{APPLIANCES};id={member}/relay"
294
+ await self.call_request(uri, method="put", data=data)
295
+ switched += 1
296
+
297
+ if switched > 0:
298
+ return requested_state
299
+
300
+ return current_state # pragma: no cover
303
301
 
304
302
  async def set_temperature(self, _: str, items: dict[str, float]) -> None:
305
303
  """Set the given Temperature on the relevant Thermostat."""
@@ -310,7 +308,7 @@ class SmileLegacyAPI(SmileLegacyData):
310
308
  if setpoint is None:
311
309
  raise PlugwiseError(
312
310
  "Plugwise: failed setting temperature: no valid input provided"
313
- ) # pragma: no cover"
311
+ ) # pragma: no cover
314
312
 
315
313
  temperature = str(setpoint)
316
314
  data = (
plugwise/smile.py CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from collections.abc import Awaitable, Callable
9
9
  import datetime as dt
10
- from typing import Any
10
+ from typing import Any, cast
11
11
 
12
12
  from plugwise.constants import (
13
13
  ADAM,
@@ -22,7 +22,10 @@ from plugwise.constants import (
22
22
  NOTIFICATIONS,
23
23
  OFF,
24
24
  RULES,
25
+ STATE_OFF,
26
+ STATE_ON,
25
27
  GwEntityData,
28
+ SwitchType,
26
29
  ThermoLoc,
27
30
  )
28
31
  from plugwise.data import SmileData
@@ -32,7 +35,27 @@ from defusedxml import ElementTree as etree
32
35
 
33
36
  # Dict as class
34
37
  from munch import Munch
35
- 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
36
59
 
37
60
 
38
61
  class SmileAPI(SmileData):
@@ -51,14 +74,7 @@ class SmileAPI(SmileData):
51
74
  _opentherm_device: bool,
52
75
  _request: Callable[..., Awaitable[Any]],
53
76
  _schedule_old_states: dict[str, dict[str, str]],
54
- smile_hostname: str | None,
55
- smile_hw_version: str | None,
56
- smile_mac_address: str | None,
57
- smile_model: str,
58
- smile_model_id: str | None,
59
- smile_name: str,
60
- smile_type: str,
61
- smile_version: Version,
77
+ smile: Munch,
62
78
  ) -> None:
63
79
  """Set the constructor for this class."""
64
80
  super().__init__()
@@ -71,14 +87,7 @@ class SmileAPI(SmileData):
71
87
  self._opentherm_device = _opentherm_device
72
88
  self._request = _request
73
89
  self._schedule_old_states = _schedule_old_states
74
- self.smile_hostname = smile_hostname
75
- self.smile_hw_version = smile_hw_version
76
- self.smile_mac_address = smile_mac_address
77
- self.smile_model = smile_model
78
- self.smile_model_id = smile_model_id
79
- self.smile_name = smile_name
80
- self.smile_type = smile_type
81
- self.smile_version = smile_version
90
+ self.smile = smile
82
91
  self.therms_with_offset_func: list[str] = []
83
92
 
84
93
  @property
@@ -104,7 +113,7 @@ class SmileAPI(SmileData):
104
113
  self.therms_with_offset_func = (
105
114
  self._get_appliances_with_offset_functionality()
106
115
  )
107
- if self.smile(ADAM):
116
+ if self.check_name(ADAM):
108
117
  self._scan_thermostats()
109
118
 
110
119
  if group_data := self._get_group_switches():
@@ -309,12 +318,12 @@ class SmileAPI(SmileData):
309
318
  Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
310
319
  """
311
320
  # Input checking
312
- if new_state not in ("on", "off"):
321
+ if new_state not in (STATE_OFF, STATE_ON):
313
322
  raise PlugwiseError("Plugwise: invalid schedule state.")
314
323
 
315
324
  # Translate selection of Off-schedule-option to disabling the active schedule
316
325
  if name == OFF:
317
- new_state = "off"
326
+ new_state = STATE_OFF
318
327
 
319
328
  # Handle no schedule-name / Off-schedule provided
320
329
  if name is None or name == OFF:
@@ -337,7 +346,7 @@ class SmileAPI(SmileData):
337
346
  template = (
338
347
  '<template tag="zone_preset_based_on_time_and_presence_with_override" />'
339
348
  )
340
- if self.smile(ANNA):
349
+ if self.check_name(ANNA):
341
350
  locator = f'.//*[@id="{schedule_rule_id}"]/template'
342
351
  template_id = self._domain_objects.find(locator).attrib["id"]
343
352
  template = f'<template id="{template_id}" />'
@@ -367,39 +376,43 @@ class SmileAPI(SmileData):
367
376
  subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
368
377
  subject = etree.fromstring(subject)
369
378
 
370
- if state == "off":
379
+ if state == STATE_OFF:
371
380
  self._last_active[loc_id] = name
372
381
  contexts.remove(subject)
373
- if state == "on":
382
+ if state == STATE_ON:
374
383
  contexts.append(subject)
375
384
 
376
385
  return str(etree.tostring(contexts, encoding="unicode").rstrip())
377
386
 
378
387
  async def set_switch_state(
379
388
  self, appl_id: str, members: list[str] | None, model: str, state: str
380
- ) -> None:
381
- """Set the given State of the relevant Switch."""
389
+ ) -> bool:
390
+ """Set the given state of the relevant Switch.
391
+
392
+ For individual switches, sets the state directly.
393
+ For group switches, sets the state for each member in the group separately.
394
+ For switch-locks, sets the lock state using a different data format.
395
+ Return the requested state when succesful, the current state otherwise.
396
+ """
397
+ model_type = cast(SwitchType, model)
398
+ current_state = self.gw_entities[appl_id]["switches"][model_type]
399
+ requested_state = state == STATE_ON
382
400
  switch = Munch()
383
401
  switch.actuator = "actuator_functionalities"
384
402
  switch.device = "relay"
385
403
  switch.func_type = "relay_functionality"
386
404
  switch.func = "state"
387
- if model == "dhw_cm_switch":
388
- switch.device = "toggle"
389
- switch.func_type = "toggle_functionality"
390
- switch.act_type = "domestic_hot_water_comfort_mode"
391
-
392
- if model == "cooling_ena_switch":
393
- switch.device = "toggle"
394
- switch.func_type = "toggle_functionality"
395
- switch.act_type = "cooling_enabled"
396
-
397
- if model == "lock":
398
- switch.func = "lock"
399
- state = "false" if state == "off" else "true"
405
+ state, switch = model_to_switch_items(model, state, switch)
406
+ data = (
407
+ f"<{switch.func_type}>"
408
+ f"<{switch.func}>{state}</{switch.func}>"
409
+ f"</{switch.func_type}>"
410
+ )
400
411
 
401
412
  if members is not None:
402
- return await self._set_groupswitch_member_state(members, state, switch)
413
+ return await self._set_groupswitch_member_state(
414
+ appl_id, data, members, state, switch
415
+ )
403
416
 
404
417
  locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
405
418
  found = self._domain_objects.findall(locator)
@@ -412,39 +425,42 @@ class SmileAPI(SmileData):
412
425
  else: # actuators with a single item like relay_functionality
413
426
  switch_id = item.attrib["id"]
414
427
 
415
- data = (
416
- f"<{switch.func_type}>"
417
- f"<{switch.func}>{state}</{switch.func}>"
418
- f"</{switch.func_type}>"
419
- )
420
428
  uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
421
429
  if model == "relay":
422
- locator = (
423
- f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
424
- )
425
- # Don't bother switching a relay when the corresponding lock-state is true
426
- if self._domain_objects.find(locator).text == "true":
427
- raise PlugwiseError("Plugwise: the locked Relay was not switched.")
430
+ lock_blocked = self.gw_entities[appl_id]["switches"].get("lock")
431
+ if lock_blocked or lock_blocked is None:
432
+ # Don't switch a relay when its corresponding lock-state is true or no
433
+ # lock is present. That means the relay can't be controlled by the user.
434
+ return current_state
428
435
 
429
436
  await self.call_request(uri, method="put", data=data)
437
+ return requested_state
430
438
 
431
439
  async def _set_groupswitch_member_state(
432
- self, members: list[str], state: str, switch: Munch
433
- ) -> None:
440
+ self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
441
+ ) -> bool:
434
442
  """Helper-function for set_switch_state().
435
443
 
436
- Set the given State of the relevant Switch within a group of members.
444
+ Set the requested state of the relevant switch within a group of switches.
445
+ Return the current group-state when none of the switches has changed its state, the requested state otherwise.
437
446
  """
447
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
448
+ requested_state = state == STATE_ON
449
+ switched = 0
438
450
  for member in members:
439
451
  locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
440
452
  switch_id = self._domain_objects.find(locator).attrib["id"]
441
453
  uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
442
- data = (
443
- f"<{switch.func_type}>"
444
- f"<{switch.func}>{state}</{switch.func}>"
445
- f"</{switch.func_type}>"
446
- )
447
- await self.call_request(uri, method="put", data=data)
454
+ lock_blocked = self.gw_entities[member]["switches"].get("lock")
455
+ # Assume Plugs under Plugwise control are not part of a group
456
+ if lock_blocked is not None and not lock_blocked:
457
+ await self.call_request(uri, method="put", data=data)
458
+ switched += 1
459
+
460
+ if switched > 0:
461
+ return requested_state
462
+
463
+ return current_state
448
464
 
449
465
  async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
450
466
  """Set the given Temperature on the relevant Thermostat."""
@@ -453,7 +469,7 @@ class SmileAPI(SmileData):
453
469
  if "setpoint" in items:
454
470
  setpoint = items["setpoint"]
455
471
 
456
- if self.smile(ANNA) and self._cooling_present:
472
+ if self.check_name(ANNA) and self._cooling_present:
457
473
  if "setpoint_high" not in items:
458
474
  raise PlugwiseError(
459
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.5
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
@@ -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=V7piQ9xOrJ8nDK-RBcMiodSX3L-vEgRwnWmmxyRZCX8,16917
4
+ plugwise/data.py,sha256=ZS6Oo05R0vXksYwsReKQLGCoyQd8bo-hg8jGE2uMH-w,12495
5
+ plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
6
+ plugwise/helper.py,sha256=LmepTNlN_EtamwZ-ur8A-MWY04X4wG1ZPN-E1f9Y1W0,38908
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=Z-7nw21s9-L4DcwPCZ_yoRGeI_fWBS1Q48kHmyh2pkY,3145
12
+ plugwise/legacy/helper.py,sha256=vUuYX-vJErbVwGJu7On1ITDh8X8nzO8WoqN-whS9FJ4,16710
13
+ plugwise/legacy/smile.py,sha256=RBfasiOrAXUWwvUqciO0xzel4VMNsY0Nzb44GHqDPpo,12837
14
+ plugwise-1.7.7.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
15
+ plugwise-1.7.7.dist-info/METADATA,sha256=dDEkjG-sCbjz4Hscb23yRpRuCT8vZkCVXZvFy2IHOTQ,7903
16
+ plugwise-1.7.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ plugwise-1.7.7.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
18
+ plugwise-1.7.7.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- plugwise/__init__.py,sha256=BKT3BagtOOy2Efk2m8uQLFPE4vizEjjPMj61Zs7MwMw,17492
2
- plugwise/common.py,sha256=_O7cC7fPGHZkrxQTaK2y8_trr9lOjEtgbS6plOfp2jk,9589
3
- plugwise/constants.py,sha256=zJFm0J14PJWasKSvepriOc6mYLljRVGNGAF5QDtzyl0,16869
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=WPZ0v45RpgJxBWEy_Sy_vsX87V4UPaVkkSgY7yZouAQ,19550
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=Q133Ub5W6VB1MyxzAZze3cFyXvTxIeIbCxSUP4eXNXM,12867
14
- plugwise-1.7.5.dist-info/licenses/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
15
- plugwise-1.7.5.dist-info/METADATA,sha256=UwtDKfDR3OtdpQ09Ra4KwD2xcQte8t9db-X81VRAkfw,7879
16
- plugwise-1.7.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- plugwise-1.7.5.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
18
- plugwise-1.7.5.dist-info/RECORD,,