plugwise 1.7.1__tar.gz → 1.7.3a1__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 (33) hide show
  1. {plugwise-1.7.1 → plugwise-1.7.3a1}/PKG-INFO +4 -3
  2. {plugwise-1.7.1 → plugwise-1.7.3a1}/README.md +3 -2
  3. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/__init__.py +10 -9
  4. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/common.py +11 -9
  5. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/data.py +8 -9
  6. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/helper.py +14 -13
  7. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/legacy/helper.py +9 -7
  8. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/legacy/smile.py +55 -25
  9. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/smile.py +60 -32
  10. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/smilecomm.py +4 -2
  11. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/util.py +5 -5
  12. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise.egg-info/PKG-INFO +4 -3
  13. {plugwise-1.7.1 → plugwise-1.7.3a1}/pyproject.toml +1 -1
  14. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_init.py +2 -0
  15. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_legacy_stretch.py +4 -0
  16. {plugwise-1.7.1 → plugwise-1.7.3a1}/LICENSE +0 -0
  17. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/constants.py +0 -0
  18. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/exceptions.py +0 -0
  19. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/legacy/data.py +0 -0
  20. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise/py.typed +0 -0
  21. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise.egg-info/SOURCES.txt +0 -0
  22. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise.egg-info/dependency_links.txt +0 -0
  23. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise.egg-info/requires.txt +0 -0
  24. {plugwise-1.7.1 → plugwise-1.7.3a1}/plugwise.egg-info/top_level.txt +0 -0
  25. {plugwise-1.7.1 → plugwise-1.7.3a1}/setup.cfg +0 -0
  26. {plugwise-1.7.1 → plugwise-1.7.3a1}/setup.py +0 -0
  27. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_adam.py +0 -0
  28. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_anna.py +0 -0
  29. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_generic.py +0 -0
  30. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_legacy_anna.py +0 -0
  31. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_legacy_generic.py +0 -0
  32. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_legacy_p1.py +0 -0
  33. {plugwise-1.7.1 → plugwise-1.7.3a1}/tests/test_p1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: plugwise
3
- Version: 1.7.1
3
+ Version: 1.7.3a1
4
4
  Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
5
5
  Home-page: https://github.com/plugwise/python-plugwise
6
6
  Author: Plugwise device owners
@@ -48,12 +48,13 @@ Requires-Dist: python-dateutil
48
48
 
49
49
  # Plugwise python module
50
50
 
51
- This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as co-code owners).
51
+ This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as code owners).
52
52
 
53
- This module supports `Smile`s (and `Stretch`), i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
53
+ This module supports Hubs such as `Adam`, `Smile`s for Anna and P1 and `Stretch`, i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
54
54
 
55
55
  Our main usage for this module is supporting [Home Assistant](https://www.home-assistant.io) / [home-assistant](http://github.com/home-assistant/core/)
56
56
 
57
+ ![Static Badge](https://img.shields.io/badge/Plugwise_Discord-Join_now-purple?style=social&logo=discord&link=https%3A%2F%2Fdiscord.gg%2FmFVhF8Ar6A)
57
58
  [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/plugwise)
58
59
  [![CodeRabbit.ai is Awesome](https://img.shields.io/badge/AI-orange?label=CodeRabbit&color=orange&link=https%3A%2F%2Fcoderabbit.ai)](https://coderabbit.ai)
59
60
  [![renovate maintained](https://img.shields.io/badge/maintained%20with-renovate-blue?logo=renovatebot)](https://github.com/plugwise/python-plugwise/issues/291)
@@ -1,11 +1,12 @@
1
1
  # Plugwise python module
2
2
 
3
- This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as co-code owners).
3
+ This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as code owners).
4
4
 
5
- This module supports `Smile`s (and `Stretch`), i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
5
+ This module supports Hubs such as `Adam`, `Smile`s for Anna and P1 and `Stretch`, i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
6
6
 
7
7
  Our main usage for this module is supporting [Home Assistant](https://www.home-assistant.io) / [home-assistant](http://github.com/home-assistant/core/)
8
8
 
9
+ ![Static Badge](https://img.shields.io/badge/Plugwise_Discord-Join_now-purple?style=social&logo=discord&link=https%3A%2F%2Fdiscord.gg%2FmFVhF8Ar6A)
9
10
  [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/plugwise)
10
11
  [![CodeRabbit.ai is Awesome](https://img.shields.io/badge/AI-orange?label=CodeRabbit&color=orange&link=https%3A%2F%2Fcoderabbit.ai)](https://coderabbit.ai)
11
12
  [![renovate maintained](https://img.shields.io/badge/maintained%20with-renovate-blue?logo=renovatebot)](https://github.com/plugwise/python-plugwise/issues/291)
@@ -198,7 +198,9 @@ class Smile(SmileComm):
198
198
 
199
199
  return self.smile_version
200
200
 
201
- async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
201
+ async def _smile_detect(
202
+ self, result: etree.Element, dsmrmain: etree.Element
203
+ ) -> None:
202
204
  """Helper-function for connect().
203
205
 
204
206
  Detect which type of Plugwise Gateway is being connected.
@@ -256,10 +258,8 @@ class Smile(SmileComm):
256
258
  # For Adam, Anna, determine the system capabilities:
257
259
  # Find the connected heating/cooling device (heater_central),
258
260
  # e.g. heat-pump or gas-fired heater
259
- onoff_boiler: etree = result.find("./module/protocols/onoff_boiler")
260
- open_therm_boiler: etree = result.find(
261
- "./module/protocols/open_therm_boiler"
262
- )
261
+ onoff_boiler = result.find("./module/protocols/onoff_boiler")
262
+ open_therm_boiler = result.find("./module/protocols/open_therm_boiler")
263
263
  self._on_off_device = onoff_boiler is not None
264
264
  self._opentherm_device = open_therm_boiler is not None
265
265
 
@@ -272,7 +272,7 @@ class Smile(SmileComm):
272
272
  self._elga = True
273
273
 
274
274
  async def _smile_detect_legacy(
275
- self, result: etree, dsmrmain: etree, model: str
275
+ self, result: etree.Element, dsmrmain: etree.Element, model: str
276
276
  ) -> str:
277
277
  """Helper-function for _smile_detect().
278
278
 
@@ -296,18 +296,19 @@ class Smile(SmileComm):
296
296
  ):
297
297
  system = await self._request(SYSTEM)
298
298
  self.smile_version = parse(system.find("./gateway/firmware").text)
299
- return_model = system.find("./gateway/product").text
299
+ return_model = str(system.find("./gateway/product").text)
300
300
  self.smile_hostname = system.find("./gateway/hostname").text
301
- # If wlan0 contains data it's active, so eth0 should be checked last
301
+ # If wlan0 contains data it's active, eth0 should be checked last as is preferred
302
302
  for network in ("wlan0", "eth0"):
303
303
  locator = f"./{network}/mac"
304
304
  if (net_locator := system.find(locator)) is not None:
305
305
  self.smile_mac_address = net_locator.text
306
+
306
307
  # P1 legacy:
307
308
  elif dsmrmain is not None:
308
309
  status = await self._request(STATUS)
309
310
  self.smile_version = parse(status.find("./system/version").text)
310
- return_model = status.find("./system/product").text
311
+ return_model = str(status.find("./system/product").text)
311
312
  self.smile_hostname = status.find("./network/hostname").text
312
313
  self.smile_mac_address = status.find("./network/mac_address").text
313
314
  else: # pragma: no cover
@@ -27,7 +27,9 @@ from defusedxml import ElementTree as etree
27
27
  from munch import Munch
28
28
 
29
29
 
30
- def get_zigbee_data(module: etree, module_data: ModuleData, legacy: bool) -> None:
30
+ def get_zigbee_data(
31
+ module: etree.Element, module_data: ModuleData, legacy: bool
32
+ ) -> None:
31
33
  """Helper-function for _get_module_data()."""
32
34
  if legacy:
33
35
  # Stretches
@@ -49,7 +51,7 @@ class SmileCommon:
49
51
  """Init."""
50
52
  self._cooling_present: bool
51
53
  self._count: int
52
- self._domain_objects: etree
54
+ self._domain_objects: etree.Element
53
55
  self._heater_id: str = NONE
54
56
  self._on_off_device: bool
55
57
  self.gw_entities: dict[str, GwEntityData] = {}
@@ -63,10 +65,10 @@ class SmileCommon:
63
65
  def _appl_heater_central_info(
64
66
  self,
65
67
  appl: Munch,
66
- xml_1: etree,
68
+ xml_1: etree.Element,
67
69
  legacy: bool,
68
- xml_2: etree = None,
69
- xml_3: etree = None,
70
+ xml_2: etree.Element = None,
71
+ xml_3: etree.Element = None,
70
72
  ) -> Munch:
71
73
  """Helper-function for _appliance_info_finder()."""
72
74
  # Find the valid heater_central
@@ -101,7 +103,7 @@ class SmileCommon:
101
103
  return appl
102
104
 
103
105
  def _appl_thermostat_info(
104
- self, appl: Munch, xml_1: etree, xml_2: etree = None
106
+ self, appl: Munch, xml_1: etree.Element, xml_2: etree.Element = None
105
107
  ) -> Munch:
106
108
  """Helper-function for _appliance_info_finder()."""
107
109
  locator = "./logs/point_log[type='thermostat']/thermostat"
@@ -190,7 +192,7 @@ class SmileCommon:
190
192
  return switch_groups
191
193
 
192
194
  def _get_lock_state(
193
- self, xml: etree, data: GwEntityData, stretch_v2: bool = False
195
+ self, xml: etree.Element, data: GwEntityData, stretch_v2: bool = False
194
196
  ) -> None:
195
197
  """Helper-function for _get_measurement_data().
196
198
 
@@ -209,9 +211,9 @@ class SmileCommon:
209
211
 
210
212
  def _get_module_data(
211
213
  self,
212
- xml_1: etree,
214
+ xml_1: etree.Element,
213
215
  locator: str,
214
- xml_2: etree = None,
216
+ xml_2: etree.Element = None,
215
217
  legacy: bool = False,
216
218
  ) -> ModuleData:
217
219
  """Helper-function for _energy_device_info_finder() and _appliance_info_finder().
@@ -228,6 +228,7 @@ class SmileData(SmileHelper):
228
228
  for msg in item.values():
229
229
  if message in msg:
230
230
  data["available"] = False
231
+ break
231
232
 
232
233
  def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None:
233
234
  """Helper-function for _get_entity_data().
@@ -329,16 +330,14 @@ class SmileData(SmileHelper):
329
330
 
330
331
  Also, replace NONE by OFF when none of the schedules are active.
331
332
  """
332
- loc_schedule_states: dict[str, str] = {}
333
- for schedule in schedules:
334
- loc_schedule_states[schedule] = "off"
335
- if schedule == selected and data["climate_mode"] == "auto":
336
- loc_schedule_states[schedule] = "on"
337
- self._schedule_old_states[location] = loc_schedule_states
338
-
339
333
  all_off = True
340
- for state in self._schedule_old_states[location].values():
341
- if state == "on":
334
+ self._schedule_old_states[location] = {}
335
+ for schedule in schedules:
336
+ active: bool = schedule == selected and data["climate_mode"] == "auto"
337
+ self._schedule_old_states[location][schedule] = "off"
338
+ if active:
339
+ self._schedule_old_states[location][schedule] = "on"
342
340
  all_off = False
341
+
343
342
  if all_off:
344
343
  data["select_schedule"] = OFF
@@ -59,7 +59,9 @@ from munch import Munch
59
59
  from packaging import version
60
60
 
61
61
 
62
- def search_actuator_functionalities(appliance: etree, actuator: str) -> etree | None:
62
+ def search_actuator_functionalities(
63
+ appliance: etree.Element, actuator: str
64
+ ) -> etree.Element | None:
63
65
  """Helper-function for finding the relevant actuator xml-structure."""
64
66
  locator = f"./actuator_functionalities/{actuator}"
65
67
  if (search := appliance.find(locator)) is not None:
@@ -195,6 +197,7 @@ class SmileHelper(SmileCommon):
195
197
  other_entities = self.gw_entities
196
198
  priority_entities = {entity_id: priority_entity}
197
199
  self.gw_entities = {**priority_entities, **other_entities}
200
+ break
198
201
 
199
202
  def _all_locations(self) -> None:
200
203
  """Collect all locations."""
@@ -212,7 +215,7 @@ class SmileHelper(SmileCommon):
212
215
  f"./location[@id='{loc.loc_id}']"
213
216
  )
214
217
 
215
- def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch:
218
+ def _appliance_info_finder(self, appl: Munch, appliance: etree.Element) -> Munch:
216
219
  """Collect info for all appliances found."""
217
220
  match appl.pwclass:
218
221
  case "gateway":
@@ -252,7 +255,7 @@ class SmileHelper(SmileCommon):
252
255
  case _: # pragma: no cover
253
256
  return appl
254
257
 
255
- def _appl_gateway_info(self, appl: Munch, appliance: etree) -> Munch:
258
+ def _appl_gateway_info(self, appl: Munch, appliance: etree.Element) -> Munch:
256
259
  """Helper-function for _appliance_info_finder()."""
257
260
  self._gateway_id = appliance.attrib["id"]
258
261
  appl.firmware = str(self.smile_version)
@@ -285,7 +288,7 @@ class SmileHelper(SmileCommon):
285
288
  return appl
286
289
 
287
290
  def _get_appl_actuator_modes(
288
- self, appliance: etree, actuator_type: str
291
+ self, appliance: etree.Element, actuator_type: str
289
292
  ) -> list[str]:
290
293
  """Get allowed modes for the given actuator type."""
291
294
  mode_list: list[str] = []
@@ -397,7 +400,7 @@ class SmileHelper(SmileCommon):
397
400
 
398
401
  def _appliance_measurements(
399
402
  self,
400
- appliance: etree,
403
+ appliance: etree.Element,
401
404
  data: GwEntityData,
402
405
  measurements: dict[str, DATA | UOM],
403
406
  ) -> None:
@@ -429,7 +432,7 @@ class SmileHelper(SmileCommon):
429
432
  self._count = count_data_items(self._count, data)
430
433
 
431
434
  def _get_toggle_state(
432
- self, xml: etree, toggle: str, name: ToggleNameType, data: GwEntityData
435
+ self, xml: etree.Element, toggle: str, name: ToggleNameType, data: GwEntityData
433
436
  ) -> None:
434
437
  """Helper-function for _get_measurement_data().
435
438
 
@@ -458,7 +461,7 @@ class SmileHelper(SmileCommon):
458
461
  )
459
462
 
460
463
  def _get_actuator_functionalities(
461
- self, xml: etree, entity: GwEntityData, data: GwEntityData
464
+ self, xml: etree.Element, entity: GwEntityData, data: GwEntityData
462
465
  ) -> None:
463
466
  """Get and process the actuator_functionalities details for an entity.
464
467
 
@@ -520,7 +523,7 @@ class SmileHelper(SmileCommon):
520
523
  data[act_item] = temp_dict
521
524
 
522
525
  def _get_actuator_mode(
523
- self, appliance: etree, entity_id: str, key: str
526
+ self, appliance: etree.Element, entity_id: str, key: str
524
527
  ) -> str | None:
525
528
  """Helper-function for _get_regulation_mode and _get_gateway_mode.
526
529
 
@@ -535,7 +538,7 @@ class SmileHelper(SmileCommon):
535
538
  return None
536
539
 
537
540
  def _get_regulation_mode(
538
- self, appliance: etree, entity_id: str, data: GwEntityData
541
+ self, appliance: etree.Element, entity_id: str, data: GwEntityData
539
542
  ) -> None:
540
543
  """Helper-function for _get_measurement_data().
541
544
 
@@ -551,7 +554,7 @@ class SmileHelper(SmileCommon):
551
554
  self._cooling_enabled = mode == "cooling"
552
555
 
553
556
  def _get_gateway_mode(
554
- self, appliance: etree, entity_id: str, data: GwEntityData
557
+ self, appliance: etree.Element, entity_id: str, data: GwEntityData
555
558
  ) -> None:
556
559
  """Helper-function for _get_measurement_data().
557
560
 
@@ -837,9 +840,7 @@ class SmileHelper(SmileCommon):
837
840
  return presets # pragma: no cover
838
841
 
839
842
  for rule_id in rule_ids:
840
- directives: etree = self._domain_objects.find(
841
- f'rule[@id="{rule_id}"]/directives'
842
- )
843
+ directives = self._domain_objects.find(f'rule[@id="{rule_id}"]/directives')
843
844
  for directive in directives:
844
845
  preset = directive.find("then").attrib
845
846
  presets[directive.attrib["preset"]] = [
@@ -23,6 +23,7 @@ from plugwise.constants import (
23
23
  NONE,
24
24
  OFF,
25
25
  P1_LEGACY_MEASUREMENTS,
26
+ PRIORITY_DEVICE_CLASSES,
26
27
  TEMP_CELSIUS,
27
28
  THERMOSTAT_CLASSES,
28
29
  UOM,
@@ -49,7 +50,7 @@ from munch import Munch
49
50
  from packaging.version import Version
50
51
 
51
52
 
52
- def etree_to_dict(element: etree) -> dict[str, str]:
53
+ def etree_to_dict(element: etree.Element) -> dict[str, str]:
53
54
  """Helper-function translating xml Element to dict."""
54
55
  node: dict[str, str] = {}
55
56
  if element is not None:
@@ -63,11 +64,11 @@ class SmileLegacyHelper(SmileCommon):
63
64
 
64
65
  def __init__(self) -> None:
65
66
  """Set the constructor for this class."""
66
- self._appliances: etree
67
+ self._appliances: etree.Element
67
68
  self._is_thermostat: bool
68
69
  self._loc_data: dict[str, ThermoLoc]
69
- self._locations: etree
70
- self._modules: etree
70
+ self._locations: etree.Element
71
+ self._modules: etree.Element
71
72
  self._stretch_v2: bool
72
73
  self.gw_entities: dict[str, GwEntityData] = {}
73
74
  self.smile_mac_address: str | None
@@ -130,7 +131,7 @@ class SmileLegacyHelper(SmileCommon):
130
131
  self._create_gw_entities(appl)
131
132
 
132
133
  # Place the gateway and optional heater_central devices as 1st and 2nd
133
- for dev_class in ("heater_central", "gateway"):
134
+ for dev_class in PRIORITY_DEVICE_CLASSES:
134
135
  for entity_id, entity in dict(self.gw_entities).items():
135
136
  if entity["dev_class"] == dev_class:
136
137
  tmp_entity = entity
@@ -138,6 +139,7 @@ class SmileLegacyHelper(SmileCommon):
138
139
  cleared_dict = self.gw_entities
139
140
  add_to_front = {entity_id: tmp_entity}
140
141
  self.gw_entities = {**add_to_front, **cleared_dict}
142
+ break
141
143
 
142
144
  def _all_locations(self) -> None:
143
145
  """Collect all locations."""
@@ -316,7 +318,7 @@ class SmileLegacyHelper(SmileCommon):
316
318
 
317
319
  def _appliance_measurements(
318
320
  self,
319
- appliance: etree,
321
+ appliance: etree.Element,
320
322
  data: GwEntityData,
321
323
  measurements: dict[str, DATA | UOM],
322
324
  ) -> None:
@@ -345,7 +347,7 @@ class SmileLegacyHelper(SmileCommon):
345
347
  self._count = count_data_items(self._count, data)
346
348
 
347
349
  def _get_actuator_functionalities(
348
- self, xml: etree, entity: GwEntityData, data: GwEntityData
350
+ self, xml: etree.Element, entity: GwEntityData, data: GwEntityData
349
351
  ) -> None:
350
352
  """Helper-function for _get_measurement_data()."""
351
353
  for item in ACTIVE_ACTUATORS:
@@ -171,9 +171,8 @@ class SmileLegacyAPI(SmileLegacyData):
171
171
  raise PlugwiseError("Plugwise: invalid preset.")
172
172
 
173
173
  locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
174
- rule = self._domain_objects.find(locator)
175
- data = f'<rules><rule id="{rule.attrib["id"]}"><active>true</active></rule></rules>'
176
-
174
+ rule_id = self._domain_objects.find(locator).attrib["id"]
175
+ data = f"<rules><rule id='{rule_id}'><active>true</active></rule></rules>"
177
176
  await self.call_request(RULES, method="put", data=data)
178
177
 
179
178
  async def set_regulation_mode(self, mode: str) -> None:
@@ -205,6 +204,7 @@ class SmileLegacyAPI(SmileLegacyData):
205
204
  for rule in self._domain_objects.findall("rule"):
206
205
  if rule.find("name").text == name:
207
206
  schedule_rule_id = rule.attrib["id"]
207
+ break
208
208
 
209
209
  if schedule_rule_id is None:
210
210
  raise PlugwiseError(
@@ -216,36 +216,68 @@ class SmileLegacyAPI(SmileLegacyData):
216
216
  new_state = "true"
217
217
 
218
218
  locator = f'.//*[@id="{schedule_rule_id}"]/template'
219
- for rule in self._domain_objects.findall(locator):
220
- template_id = rule.attrib["id"]
219
+ template_id = self._domain_objects.find(locator).attrib["id"]
221
220
 
222
- uri = f"{RULES};id={schedule_rule_id}"
223
221
  data = (
224
- "<rules><rule"
225
- f' id="{schedule_rule_id}"><name><![CDATA[{name}]]></name><template'
226
- f' id="{template_id}" /><active>{new_state}</active></rule></rules>'
222
+ "<rules>"
223
+ f"<rule id='{schedule_rule_id}'>"
224
+ f"<name><![CDATA[{name}]]></name>"
225
+ f"<template id='{template_id}' />"
226
+ f"<active>{new_state}</active>"
227
+ "</rule>"
228
+ "</rules>"
227
229
  )
228
-
230
+ uri = f"{RULES};id={schedule_rule_id}"
229
231
  await self.call_request(uri, method="put", data=data)
230
232
 
231
233
  async def set_switch_state(
232
234
  self, appl_id: str, members: list[str] | None, model: str, state: str
233
235
  ) -> None:
234
- """Set the given State of the relevant Switch."""
236
+ """Set the given state of the relevant switch.
237
+
238
+ For individual switches, sets the state directly.
239
+ For group switches, sets the state for each member in the group separately.
240
+ For switch-locks, sets the lock state using a different data format.
241
+ """
235
242
  switch = Munch()
236
243
  switch.actuator = "actuator_functionalities"
237
244
  switch.func_type = "relay_functionality"
238
245
  if self._stretch_v2:
239
246
  switch.actuator = "actuators"
240
247
  switch.func_type = "relay"
241
- switch.func = "state"
242
248
 
243
- if members is not None:
244
- return await self._set_groupswitch_member_state(members, state, switch)
249
+ # Handle switch-lock
250
+ if model == "lock":
251
+ state = "false" if state == "off" else "true"
252
+ appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
253
+ appl_name = appliance.find("name").text
254
+ appl_type = appliance.find("type").text
255
+ data = (
256
+ "<appliances>"
257
+ f"<appliance id='{appl_id}'>"
258
+ f"<name><![CDATA[{appl_name}]]></name>"
259
+ f"<description><![CDATA[]]></description>"
260
+ f"<type><![CDATA[{appl_type}]]></type>"
261
+ f"<{switch.actuator}>"
262
+ f"<{switch.func_type}>"
263
+ f"<lock>{state}</lock>"
264
+ f"</{switch.func_type}>"
265
+ f"</{switch.actuator}>"
266
+ "</appliance>"
267
+ "</appliances>"
268
+ )
269
+ await self.call_request(APPLIANCES, method="post", data=data)
270
+ return
245
271
 
246
- data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
247
- uri = f"{APPLIANCES};id={appl_id}/{switch.func_type}"
272
+ # Handle group of switches
273
+ data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
274
+ if members is not None:
275
+ return await self._set_groupswitch_member_state(
276
+ data, members, state, switch
277
+ )
248
278
 
279
+ # Handle individual relay switches
280
+ uri = f"{APPLIANCES};id={appl_id}/relay"
249
281
  if model == "relay":
250
282
  locator = (
251
283
  f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
@@ -257,16 +289,14 @@ class SmileLegacyAPI(SmileLegacyData):
257
289
  await self.call_request(uri, method="put", data=data)
258
290
 
259
291
  async def _set_groupswitch_member_state(
260
- self, members: list[str], state: str, switch: Munch
292
+ self, data: str, members: list[str], state: str, switch: Munch
261
293
  ) -> None:
262
294
  """Helper-function for set_switch_state().
263
295
 
264
- Set the given State of the relevant Switch within a group of members.
296
+ Set the given State of the relevant Switch (relay) within a group of members.
265
297
  """
266
298
  for member in members:
267
- uri = f"{APPLIANCES};id={member}/{switch.func_type}"
268
- data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
269
-
299
+ uri = f"{APPLIANCES};id={member}/relay"
270
300
  await self.call_request(uri, method="put", data=data)
271
301
 
272
302
  async def set_temperature(self, _: str, items: dict[str, float]) -> None:
@@ -281,12 +311,12 @@ class SmileLegacyAPI(SmileLegacyData):
281
311
  ) # pragma: no cover"
282
312
 
283
313
  temperature = str(setpoint)
284
- uri = self._thermostat_uri()
285
314
  data = (
286
- "<thermostat_functionality><setpoint>"
287
- f"{temperature}</setpoint></thermostat_functionality>"
315
+ "<thermostat_functionality>"
316
+ f"<setpoint>{temperature}</setpoint>"
317
+ "</thermostat_functionality>"
288
318
  )
289
-
319
+ uri = self._thermostat_uri()
290
320
  await self.call_request(uri, method="put", data=data)
291
321
 
292
322
  async def call_request(self, uri: str, **kwargs: Any) -> None:
@@ -174,8 +174,12 @@ class SmileAPI(SmileData):
174
174
  if thermostat_id is None:
175
175
  raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
176
176
 
177
+ data = (
178
+ "<thermostat_functionality>"
179
+ f"<setpoint>{temp}</setpoint>"
180
+ "</thermostat_functionality>"
181
+ )
177
182
  uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
178
- data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
179
183
  await self.call_request(uri, method="put", data=data)
180
184
 
181
185
  async def set_offset(self, dev_id: str, offset: float) -> None:
@@ -186,9 +190,8 @@ class SmileAPI(SmileData):
186
190
  )
187
191
 
188
192
  value = str(offset)
189
- uri = f"{APPLIANCES};id={dev_id}/offset;type=temperature_offset"
190
193
  data = f"<offset_functionality><offset>{value}</offset></offset_functionality>"
191
-
194
+ uri = f"{APPLIANCES};id={dev_id}/offset;type=temperature_offset"
192
195
  await self.call_request(uri, method="put", data=data)
193
196
 
194
197
  async def set_preset(self, loc_id: str, preset: str) -> None:
@@ -201,14 +204,16 @@ class SmileAPI(SmileData):
201
204
  current_location = self._domain_objects.find(f'location[@id="{loc_id}"]')
202
205
  location_name = current_location.find("name").text
203
206
  location_type = current_location.find("type").text
204
-
205
- uri = f"{LOCATIONS};id={loc_id}"
206
207
  data = (
207
- "<locations><location"
208
- f' id="{loc_id}"><name>{location_name}</name><type>{location_type}'
209
- f"</type><preset>{preset}</preset></location></locations>"
208
+ "<locations>"
209
+ f'<location id="{loc_id}">'
210
+ f"<name>{location_name}</name>"
211
+ f"<type>{location_type}</type>"
212
+ f"<preset>{preset}</preset>"
213
+ "</location>"
214
+ "</locations>"
210
215
  )
211
-
216
+ uri = f"{LOCATIONS};id={loc_id}"
212
217
  await self.call_request(uri, method="put", data=data)
213
218
 
214
219
  async def set_select(
@@ -231,9 +236,12 @@ class SmileAPI(SmileData):
231
236
  if mode not in self._dhw_allowed_modes:
232
237
  raise PlugwiseError("Plugwise: invalid dhw mode.")
233
238
 
239
+ data = (
240
+ "<domestic_hot_water_mode_control_functionality>"
241
+ f"<mode>{mode}</mode>"
242
+ "</domestic_hot_water_mode_control_functionality>"
243
+ )
234
244
  uri = f"{APPLIANCES};type=heater_central/domestic_hot_water_mode_control"
235
- data = f"<domestic_hot_water_mode_control_functionality><mode>{mode}</mode></domestic_hot_water_mode_control_functionality>"
236
-
237
245
  await self.call_request(uri, method="put", data=data)
238
246
 
239
247
  async def set_gateway_mode(self, mode: str) -> None:
@@ -259,9 +267,13 @@ class SmileAPI(SmileData):
259
267
  vacation_time = time_2 + "T23:00:00.000Z"
260
268
  valid = f"<valid_from>{vacation_time}</valid_from><valid_to>{end_time}</valid_to>"
261
269
 
270
+ data = (
271
+ "<gateway_mode_control_functionality>"
272
+ f"<mode>{mode}</mode>"
273
+ f"{valid}"
274
+ "</gateway_mode_control_functionality>"
275
+ )
262
276
  uri = f"{APPLIANCES};id={self._smile_props['gateway_id']}/gateway_mode_control"
263
- data = f"<gateway_mode_control_functionality><mode>{mode}</mode>{valid}</gateway_mode_control_functionality>"
264
-
265
277
  await self.call_request(uri, method="put", data=data)
266
278
 
267
279
  async def set_regulation_mode(self, mode: str) -> None:
@@ -269,12 +281,17 @@ class SmileAPI(SmileData):
269
281
  if mode not in self._reg_allowed_modes:
270
282
  raise PlugwiseError("Plugwise: invalid regulation mode.")
271
283
 
272
- uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
273
284
  duration = ""
274
285
  if "bleeding" in mode:
275
286
  duration = "<duration>300</duration>"
276
- data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
277
287
 
288
+ data = (
289
+ "<regulation_mode_control_functionality>"
290
+ f"{duration}"
291
+ f"<mode>{mode}</mode>"
292
+ "</regulation_mode_control_functionality>"
293
+ )
294
+ uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
278
295
  await self.call_request(uri, method="put", data=data)
279
296
 
280
297
  async def set_schedule_state(
@@ -323,18 +340,22 @@ class SmileAPI(SmileData):
323
340
  template = f'<template id="{template_id}" />'
324
341
 
325
342
  contexts = self.determine_contexts(loc_id, name, new_state, schedule_rule_id)
326
- uri = f"{RULES};id={schedule_rule_id}"
327
343
  data = (
328
- f'<rules><rule id="{schedule_rule_id}"><name><![CDATA[{name}]]></name>'
329
- f"{template}{contexts}</rule></rules>"
344
+ "<rules>"
345
+ f"<rule id='{schedule_rule_id}'>"
346
+ f"<name><![CDATA[{name}]]></name>"
347
+ f"{template}"
348
+ f"{contexts}"
349
+ "</rule>"
350
+ "</rules>"
330
351
  )
331
-
352
+ uri = f"{RULES};id={schedule_rule_id}"
332
353
  await self.call_request(uri, method="put", data=data)
333
354
  self._schedule_old_states[loc_id][name] = new_state
334
355
 
335
356
  def determine_contexts(
336
357
  self, loc_id: str, name: str, state: str, sched_id: str
337
- ) -> etree:
358
+ ) -> str:
338
359
  """Helper-function for set_schedule_state()."""
339
360
  locator = f'.//*[@id="{sched_id}"]/contexts'
340
361
  contexts = self._domain_objects.find(locator)
@@ -349,7 +370,7 @@ class SmileAPI(SmileData):
349
370
  if state == "on":
350
371
  contexts.append(subject)
351
372
 
352
- return etree.tostring(contexts, encoding="unicode").rstrip()
373
+ return str(etree.tostring(contexts, encoding="unicode").rstrip())
353
374
 
354
375
  async def set_switch_state(
355
376
  self, appl_id: str, members: list[str] | None, model: str, state: str
@@ -378,18 +399,22 @@ class SmileAPI(SmileData):
378
399
  return await self._set_groupswitch_member_state(members, state, switch)
379
400
 
380
401
  locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
381
- found: list[etree] = self._domain_objects.findall(locator)
402
+ found = self._domain_objects.findall(locator)
382
403
  for item in found:
404
+ # multiple types of e.g. toggle_functionality present
383
405
  if (sw_type := item.find("type")) is not None:
384
406
  if sw_type.text == switch.act_type:
385
407
  switch_id = item.attrib["id"]
386
- else:
408
+ break
409
+ else: # actuators with a single item like relay_functionality
387
410
  switch_id = item.attrib["id"]
388
- break
389
411
 
412
+ data = (
413
+ f"<{switch.func_type}>"
414
+ f"<{switch.func}>{state}</{switch.func}>"
415
+ f"</{switch.func_type}>"
416
+ )
390
417
  uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
391
- data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
392
-
393
418
  if model == "relay":
394
419
  locator = (
395
420
  f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
@@ -411,8 +436,11 @@ class SmileAPI(SmileData):
411
436
  locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
412
437
  switch_id = self._domain_objects.find(locator).attrib["id"]
413
438
  uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
414
- data = f"<{switch.func_type}><{switch.func}>{state}</{switch.func}></{switch.func_type}>"
415
-
439
+ data = (
440
+ f"<{switch.func_type}>"
441
+ f"<{switch.func}>{state}</{switch.func}>"
442
+ f"</{switch.func_type}>"
443
+ )
416
444
  await self.call_request(uri, method="put", data=data)
417
445
 
418
446
  async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
@@ -448,12 +476,12 @@ class SmileAPI(SmileData):
448
476
  ) # pragma: no cover"
449
477
 
450
478
  temperature = str(setpoint)
451
- uri = self._thermostat_uri(loc_id)
452
479
  data = (
453
- "<thermostat_functionality><setpoint>"
454
- f"{temperature}</setpoint></thermostat_functionality>"
480
+ "<thermostat_functionality>"
481
+ f"<setpoint>{temperature}</setpoint>"
482
+ "</thermostat_functionality>"
455
483
  )
456
-
484
+ uri = self._thermostat_uri(loc_id)
457
485
  await self.call_request(uri, method="put", data=data)
458
486
 
459
487
  async def call_request(self, uri: str, **kwargs: Any) -> None:
@@ -51,7 +51,7 @@ class SmileComm:
51
51
  retry: int = 3,
52
52
  method: str = "get",
53
53
  data: str | None = None,
54
- ) -> etree:
54
+ ) -> etree.Element:
55
55
  """Get/put/delete data from a give URL."""
56
56
  resp: ClientResponse
57
57
  url = f"{self._endpoint}{command}"
@@ -107,7 +107,9 @@ class SmileComm:
107
107
 
108
108
  return await self._request_validate(resp, method)
109
109
 
110
- async def _request_validate(self, resp: ClientResponse, method: str) -> etree:
110
+ async def _request_validate(
111
+ self, resp: ClientResponse, method: str
112
+ ) -> etree.Element:
111
113
  """Helper-function for _request(): validate the returned data."""
112
114
  match resp.status:
113
115
  case 200:
@@ -74,7 +74,7 @@ def in_alternative_location(loc: Munch, legacy: bool) -> bool:
74
74
  return present
75
75
 
76
76
 
77
- def check_heater_central(xml: etree) -> str:
77
+ def check_heater_central(xml: etree.Element) -> str:
78
78
  """Find the valid heater_central, helper-function for _appliance_info_finder().
79
79
 
80
80
  Solution for Core Issue #104433,
@@ -143,7 +143,7 @@ def collect_power_values(
143
143
  def common_match_cases(
144
144
  measurement: str,
145
145
  attrs: DATA | UOM,
146
- location: etree,
146
+ location: etree.Element,
147
147
  data: GwEntityData,
148
148
  ) -> None:
149
149
  """Helper-function for common match-case execution."""
@@ -213,7 +213,7 @@ def format_measure(measure: str, unit: str) -> float | int:
213
213
  return result
214
214
 
215
215
 
216
- def get_vendor_name(module: etree, model_data: ModuleData) -> ModuleData:
216
+ def get_vendor_name(module: etree.Element, model_data: ModuleData) -> ModuleData:
217
217
  """Helper-function for _get_model_data()."""
218
218
  if (vendor_name := module.find("vendor_name").text) is not None:
219
219
  model_data["vendor_name"] = vendor_name
@@ -302,12 +302,12 @@ def remove_empty_platform_dicts(data: GwEntityData) -> None:
302
302
  data.pop("switches")
303
303
 
304
304
 
305
- def return_valid(value: etree | None, default: etree) -> etree:
305
+ def return_valid(value: etree.Element | None, default: etree.Element) -> etree.Element:
306
306
  """Return default when value is None."""
307
307
  return value if value is not None else default
308
308
 
309
309
 
310
- def skip_obsolete_measurements(xml: etree, measurement: str) -> bool:
310
+ def skip_obsolete_measurements(xml: etree.Element, measurement: str) -> bool:
311
311
  """Skipping known obsolete measurements."""
312
312
  locator = f".//logs/point_log[type='{measurement}']/updated_date"
313
313
  if (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: plugwise
3
- Version: 1.7.1
3
+ Version: 1.7.3a1
4
4
  Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
5
5
  Home-page: https://github.com/plugwise/python-plugwise
6
6
  Author: Plugwise device owners
@@ -48,12 +48,13 @@ Requires-Dist: python-dateutil
48
48
 
49
49
  # Plugwise python module
50
50
 
51
- This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as co-code owners).
51
+ This module is the backend for the [`plugwise` component](https://github.com/home-assistant/core/tree/dev/homeassistant/components/plugwise) in Home Assistant Core (which we maintain as code owners).
52
52
 
53
- This module supports `Smile`s (and `Stretch`), i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
53
+ This module supports Hubs such as `Adam`, `Smile`s for Anna and P1 and `Stretch`, i.e. the networked plugwise devices. For the USB (or Stick-standalone version) please refer to upcoming [`plugwise-usb` component](https://github.com/plugwise/plugwise_usb-beta).
54
54
 
55
55
  Our main usage for this module is supporting [Home Assistant](https://www.home-assistant.io) / [home-assistant](http://github.com/home-assistant/core/)
56
56
 
57
+ ![Static Badge](https://img.shields.io/badge/Plugwise_Discord-Join_now-purple?style=social&logo=discord&link=https%3A%2F%2Fdiscord.gg%2FmFVhF8Ar6A)
57
58
  [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/plugwise)
58
59
  [![CodeRabbit.ai is Awesome](https://img.shields.io/badge/AI-orange?label=CodeRabbit&color=orange&link=https%3A%2F%2Fcoderabbit.ai)](https://coderabbit.ai)
59
60
  [![renovate maintained](https://img.shields.io/badge/maintained%20with-renovate-blue?logo=renovatebot)](https://github.com/plugwise/python-plugwise/issues/291)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "plugwise"
7
- version = "1.7.1"
7
+ version = "1.7.3a1"
8
8
  license = {file = "LICENSE"}
9
9
  description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
10
10
  readme = "README.md"
@@ -167,8 +167,10 @@ class TestPlugwise: # pylint: disable=attribute-defined-outside-init
167
167
  "PUT", CORE_APPLIANCES_TAIL, self.smile_http_accept
168
168
  )
169
169
  else:
170
+ app.router.add_route("POST", CORE_APPLIANCES_TAIL, self.smile_http_ok)
170
171
  app.router.add_route("PUT", CORE_APPLIANCES_TAIL, self.smile_http_ok)
171
172
  else:
173
+ app.router.add_route("POST", CORE_APPLIANCES_TAIL, self.smile_timeout)
172
174
  app.router.add_route("PUT", CORE_LOCATIONS_TAIL, self.smile_timeout)
173
175
  app.router.add_route("PUT", CORE_RULES_TAIL, self.smile_timeout)
174
176
  app.router.add_route("PUT", CORE_APPLIANCES_TAIL, self.smile_timeout)
@@ -74,6 +74,10 @@ class TestPlugwiseStretch(TestPlugwise): # pylint: disable=attribute-defined-ou
74
74
  smile, "2587a7fcdd7e482dab03fda256076b4b"
75
75
  )
76
76
  assert switch_change
77
+ switch_change = await self.tinker_switch(
78
+ smile, "2587a7fcdd7e482dab03fda256076b4b", model="lock"
79
+ )
80
+ assert switch_change
77
81
  switch_change = await self.tinker_switch(
78
82
  smile,
79
83
  "f7b145c8492f4dd7a4de760456fdef3e",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes