plugwise 1.7.5__tar.gz → 1.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {plugwise-1.7.5 → plugwise-1.7.6}/PKG-INFO +1 -1
  2. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/__init__.py +16 -3
  3. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/constants.py +2 -0
  4. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/legacy/smile.py +31 -18
  5. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/smile.py +52 -29
  6. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise.egg-info/PKG-INFO +1 -1
  7. {plugwise-1.7.5 → plugwise-1.7.6}/pyproject.toml +1 -1
  8. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_adam.py +24 -3
  9. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_init.py +23 -7
  10. {plugwise-1.7.5 → plugwise-1.7.6}/LICENSE +0 -0
  11. {plugwise-1.7.5 → plugwise-1.7.6}/README.md +0 -0
  12. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/common.py +0 -0
  13. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/data.py +0 -0
  14. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/exceptions.py +0 -0
  15. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/helper.py +0 -0
  16. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/legacy/data.py +0 -0
  17. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/legacy/helper.py +0 -0
  18. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/py.typed +0 -0
  19. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/smilecomm.py +0 -0
  20. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise/util.py +0 -0
  21. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise.egg-info/SOURCES.txt +0 -0
  22. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise.egg-info/dependency_links.txt +0 -0
  23. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise.egg-info/requires.txt +0 -0
  24. {plugwise-1.7.5 → plugwise-1.7.6}/plugwise.egg-info/top_level.txt +0 -0
  25. {plugwise-1.7.5 → plugwise-1.7.6}/setup.cfg +0 -0
  26. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_anna.py +0 -0
  27. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_generic.py +0 -0
  28. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_legacy_anna.py +0 -0
  29. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_legacy_generic.py +0 -0
  30. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_legacy_p1.py +0 -0
  31. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_legacy_stretch.py +0 -0
  32. {plugwise-1.7.5 → plugwise-1.7.6}/tests/test_p1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugwise
3
- Version: 1.7.5
3
+ Version: 1.7.6
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
@@ -15,6 +15,8 @@ from plugwise.constants import (
15
15
  MODULES,
16
16
  NONE,
17
17
  SMILES,
18
+ STATE_OFF,
19
+ STATE_ON,
18
20
  STATUS,
19
21
  SYSTEM,
20
22
  GwEntityData,
@@ -398,10 +400,21 @@ class Smile(SmileComm):
398
400
 
399
401
  async def set_switch_state(
400
402
  self, appl_id: str, members: list[str] | None, model: str, state: str
401
- ) -> None:
402
- """Set the given State of the relevant Switch."""
403
+ ) -> bool:
404
+ """Set the given State of the relevant Switch.
405
+
406
+ Return the result:
407
+ - True when switched to state on,
408
+ - False when switched to state off,
409
+ - the unchanged state when the switch is for instance locked.
410
+ """
411
+ if state not in (STATE_OFF, STATE_ON):
412
+ raise PlugwiseError("Invalid state supplied to set_switch_state")
413
+
403
414
  try:
404
- await self._smile_api.set_switch_state(appl_id, members, model, state)
415
+ return await self._smile_api.set_switch_state(
416
+ appl_id, members, model, state
417
+ )
405
418
  except ConnectionFailedError as exc:
406
419
  raise ConnectionFailedError(
407
420
  f"Failed to set switch state: {str(exc)}"
@@ -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"
@@ -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
  )
@@ -195,7 +197,7 @@ class SmileLegacyAPI(SmileLegacyData):
195
197
  Determined from - DOMAIN_OBJECTS.
196
198
  Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
197
199
  """
198
- if state not in ("on", "off"):
200
+ if state not in (STATE_OFF, STATE_ON):
199
201
  raise PlugwiseError("Plugwise: invalid schedule state.")
200
202
 
201
203
  # Handle no schedule-name / Off-schedule provided
@@ -214,7 +216,7 @@ class SmileLegacyAPI(SmileLegacyData):
214
216
  ) # pragma: no cover
215
217
 
216
218
  new_state = "false"
217
- if state == "on":
219
+ if state == STATE_ON:
218
220
  new_state = "true"
219
221
 
220
222
  locator = f'.//*[@id="{schedule_rule_id}"]/template'
@@ -234,13 +236,16 @@ class SmileLegacyAPI(SmileLegacyData):
234
236
 
235
237
  async def set_switch_state(
236
238
  self, appl_id: str, members: list[str] | None, model: str, state: str
237
- ) -> None:
239
+ ) -> bool:
238
240
  """Set the given state of the relevant switch.
239
241
 
240
242
  For individual switches, sets the state directly.
241
243
  For group switches, sets the state for each member in the group separately.
242
244
  For switch-locks, sets the lock state using a different data format.
245
+ Return the requested state when succesful, the current state otherwise.
243
246
  """
247
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
248
+ requested_state = state == STATE_ON
244
249
  switch = Munch()
245
250
  switch.actuator = "actuator_functionalities"
246
251
  switch.func_type = "relay_functionality"
@@ -250,7 +255,7 @@ class SmileLegacyAPI(SmileLegacyData):
250
255
 
251
256
  # Handle switch-lock
252
257
  if model == "lock":
253
- state = "false" if state == "off" else "true"
258
+ state = "true" if state == STATE_ON else "false"
254
259
  appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
255
260
  appl_name = appliance.find("name").text
256
261
  appl_type = appliance.find("type").text
@@ -269,37 +274,45 @@ class SmileLegacyAPI(SmileLegacyData):
269
274
  "</appliances>"
270
275
  )
271
276
  await self.call_request(APPLIANCES, method="post", data=data)
272
- return
277
+ return requested_state
273
278
 
274
279
  # Handle group of switches
275
280
  data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
276
281
  if members is not None:
277
282
  return await self._set_groupswitch_member_state(
278
- data, members, state, switch
283
+ appl_id, data, members, state, switch
279
284
  )
280
285
 
281
286
  # Handle individual relay switches
282
287
  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
- )
288
+ if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]:
287
289
  # 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.")
290
+ return current_state
290
291
 
291
292
  await self.call_request(uri, method="put", data=data)
293
+ return requested_state
292
294
 
293
295
  async def _set_groupswitch_member_state(
294
- self, data: str, members: list[str], state: str, switch: Munch
295
- ) -> None:
296
+ self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
297
+ ) -> bool:
296
298
  """Helper-function for set_switch_state().
297
299
 
298
- Set the given State of the relevant Switch (relay) within a group of members.
300
+ Set the requested state of the relevant switch within a group of switches.
301
+ Return the current group-state when none of the switches has changed its state, the requested state otherwise.
299
302
  """
303
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
304
+ requested_state = state == STATE_ON
305
+ switched = 0
300
306
  for member in members:
301
- uri = f"{APPLIANCES};id={member}/relay"
302
- await self.call_request(uri, method="put", data=data)
307
+ if not self.gw_entities[member]["switches"]["lock"]:
308
+ uri = f"{APPLIANCES};id={member}/relay"
309
+ await self.call_request(uri, method="put", data=data)
310
+ switched += 1
311
+
312
+ if switched > 0:
313
+ return requested_state
314
+
315
+ return current_state # pragma: no cover
303
316
 
304
317
  async def set_temperature(self, _: str, items: dict[str, float]) -> None:
305
318
  """Set the given Temperature on the relevant Thermostat."""
@@ -310,7 +323,7 @@ class SmileLegacyAPI(SmileLegacyData):
310
323
  if setpoint is None:
311
324
  raise PlugwiseError(
312
325
  "Plugwise: failed setting temperature: no valid input provided"
313
- ) # pragma: no cover"
326
+ ) # pragma: no cover
314
327
 
315
328
  temperature = str(setpoint)
316
329
  data = (
@@ -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
@@ -309,12 +312,12 @@ class SmileAPI(SmileData):
309
312
  Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
310
313
  """
311
314
  # Input checking
312
- if new_state not in ("on", "off"):
315
+ if new_state not in (STATE_OFF, STATE_ON):
313
316
  raise PlugwiseError("Plugwise: invalid schedule state.")
314
317
 
315
318
  # Translate selection of Off-schedule-option to disabling the active schedule
316
319
  if name == OFF:
317
- new_state = "off"
320
+ new_state = STATE_OFF
318
321
 
319
322
  # Handle no schedule-name / Off-schedule provided
320
323
  if name is None or name == OFF:
@@ -367,18 +370,27 @@ class SmileAPI(SmileData):
367
370
  subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
368
371
  subject = etree.fromstring(subject)
369
372
 
370
- if state == "off":
373
+ if state == STATE_OFF:
371
374
  self._last_active[loc_id] = name
372
375
  contexts.remove(subject)
373
- if state == "on":
376
+ if state == STATE_ON:
374
377
  contexts.append(subject)
375
378
 
376
379
  return str(etree.tostring(contexts, encoding="unicode").rstrip())
377
380
 
378
381
  async def set_switch_state(
379
382
  self, appl_id: str, members: list[str] | None, model: str, state: str
380
- ) -> None:
381
- """Set the given State of the relevant Switch."""
383
+ ) -> bool:
384
+ """Set the given state of the relevant Switch.
385
+
386
+ For individual switches, sets the state directly.
387
+ For group switches, sets the state for each member in the group separately.
388
+ For switch-locks, sets the lock state using a different data format.
389
+ Return the requested state when succesful, the current state otherwise.
390
+ """
391
+ model_type = cast(SwitchType, model)
392
+ current_state = self.gw_entities[appl_id]["switches"][model_type]
393
+ requested_state = state == STATE_ON
382
394
  switch = Munch()
383
395
  switch.actuator = "actuator_functionalities"
384
396
  switch.device = "relay"
@@ -396,10 +408,18 @@ class SmileAPI(SmileData):
396
408
 
397
409
  if model == "lock":
398
410
  switch.func = "lock"
399
- state = "false" if state == "off" else "true"
411
+ state = "true" if state == STATE_ON else "false"
412
+
413
+ data = (
414
+ f"<{switch.func_type}>"
415
+ f"<{switch.func}>{state}</{switch.func}>"
416
+ f"</{switch.func_type}>"
417
+ )
400
418
 
401
419
  if members is not None:
402
- return await self._set_groupswitch_member_state(members, state, switch)
420
+ return await self._set_groupswitch_member_state(
421
+ appl_id, data, members, state, switch
422
+ )
403
423
 
404
424
  locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
405
425
  found = self._domain_objects.findall(locator)
@@ -412,39 +432,42 @@ class SmileAPI(SmileData):
412
432
  else: # actuators with a single item like relay_functionality
413
433
  switch_id = item.attrib["id"]
414
434
 
415
- data = (
416
- f"<{switch.func_type}>"
417
- f"<{switch.func}>{state}</{switch.func}>"
418
- f"</{switch.func_type}>"
419
- )
420
435
  uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
421
436
  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.")
437
+ lock_blocked = self.gw_entities[appl_id]["switches"].get("lock")
438
+ if lock_blocked or lock_blocked is None:
439
+ # Don't switch a relay when its corresponding lock-state is true or no
440
+ # lock is present. That means the relay can't be controlled by the user.
441
+ return current_state
428
442
 
429
443
  await self.call_request(uri, method="put", data=data)
444
+ return requested_state
430
445
 
431
446
  async def _set_groupswitch_member_state(
432
- self, members: list[str], state: str, switch: Munch
433
- ) -> None:
447
+ self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
448
+ ) -> bool:
434
449
  """Helper-function for set_switch_state().
435
450
 
436
- Set the given State of the relevant Switch within a group of members.
451
+ Set the requested state of the relevant switch within a group of switches.
452
+ Return the current group-state when none of the switches has changed its state, the requested state otherwise.
437
453
  """
454
+ current_state = self.gw_entities[appl_id]["switches"]["relay"]
455
+ requested_state = state == STATE_ON
456
+ switched = 0
438
457
  for member in members:
439
458
  locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
440
459
  switch_id = self._domain_objects.find(locator).attrib["id"]
441
460
  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)
461
+ lock_blocked = self.gw_entities[member]["switches"].get("lock")
462
+ # Assume Plugs under Plugwise control are not part of a group
463
+ if lock_blocked is not None and not lock_blocked:
464
+ await self.call_request(uri, method="put", data=data)
465
+ switched += 1
466
+
467
+ if switched > 0:
468
+ return requested_state
469
+
470
+ return current_state
448
471
 
449
472
  async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
450
473
  """Set the given Temperature on the relevant Thermostat."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugwise
3
- Version: 1.7.5
3
+ Version: 1.7.6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "plugwise"
7
- version = "1.7.5"
7
+ version = "1.7.6"
8
8
  license = "MIT"
9
9
  description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
10
10
  readme = "README.md"
@@ -106,14 +106,27 @@ class TestPlugwiseAdam(TestPlugwise): # pylint: disable=attribute-defined-outsi
106
106
  smile, "056ee145a816487eaa69243c3280f8bf", model="dhw_cm_switch"
107
107
  )
108
108
  assert switch_change
109
+ # Test relay without lock-attribute
109
110
  switch_change = await self.tinker_switch(
110
- smile, "854f8a9b0e7e425db97f1f110e1ce4b3", model="lock"
111
+ smile,
112
+ "854f8a9b0e7e425db97f1f110e1ce4b3",
111
113
  )
112
- assert switch_change
114
+ assert not switch_change
113
115
  switch_change = await self.tinker_switch(
114
116
  smile, "2568cc4b9c1e401495d4741a5f89bee1"
115
117
  )
116
118
  assert not switch_change
119
+ switch_change = await self.tinker_switch(
120
+ smile,
121
+ "2568cc4b9c1e401495d4741a5f89bee1",
122
+ model="lock",
123
+ )
124
+ assert switch_change
125
+
126
+ assert await self.tinker_switch_bad_input(
127
+ smile,
128
+ "854f8a9b0e7e425db97f1f110e1ce4b3",
129
+ )
117
130
 
118
131
  tinkered = await self.tinker_gateway_mode(smile)
119
132
  assert not tinkered
@@ -288,7 +301,7 @@ class TestPlugwiseAdam(TestPlugwise): # pylint: disable=attribute-defined-outsi
288
301
  assert smile._last_active["82fa13f017d240daa0d0ea1775420f24"] == CV_JESSIE
289
302
  assert smile._last_active["08963fec7c53423ca5680aa4cb502c63"] == BADKAMER_SCHEMA
290
303
  assert smile._last_active["446ac08dd04d4eff8ac57489757b7314"] == BADKAMER_SCHEMA
291
- assert self.entity_items == 370
304
+ assert self.entity_items == 375
292
305
 
293
306
  assert "af82e4ccf9c548528166d38e560662a4" in self.notifications
294
307
 
@@ -304,6 +317,14 @@ class TestPlugwiseAdam(TestPlugwise): # pylint: disable=attribute-defined-outsi
304
317
  smile, "675416a629f343c495449970e2ca37b5"
305
318
  )
306
319
  assert not switch_change
320
+ # Test a blocked group-change, both relays are locked.
321
+ group_change = await self.tinker_switch(
322
+ smile,
323
+ "e8ef2a01ed3b4139a53bf749204fe6b4",
324
+ ["02cf28bfec924855854c544690a609ef", "4a810418d5394b3f82727340b91ba740"],
325
+ )
326
+ assert not group_change
327
+
307
328
  await smile.close_connection()
308
329
  await self.disconnect(server, client)
309
330
 
@@ -684,16 +684,18 @@ class TestPlugwise: # pylint: disable=attribute-defined-outside-init
684
684
  """Turn a Switch on and off to test functionality."""
685
685
  _LOGGER.info("Asserting modifying settings for switch devices:")
686
686
  _LOGGER.info("- Devices (%s):", dev_id)
687
+ convert = {"on": True, "off": False}
687
688
  tinker_switch_passed = False
688
- for new_state in ["false", "true", "false"]:
689
+ for new_state in ["off", "on", "off"]:
689
690
  _LOGGER.info("- Switching %s", new_state)
690
691
  try:
691
- await smile.set_switch_state(dev_id, members, model, new_state)
692
- tinker_switch_passed = True
693
- _LOGGER.info(" + tinker_switch worked as intended")
694
- except pw_exceptions.PlugwiseError:
695
- _LOGGER.info(" + locked, not switched as expected")
696
- return False
692
+ result = await smile.set_switch_state(dev_id, members, model, new_state)
693
+ if result == convert[new_state]:
694
+ tinker_switch_passed = True
695
+ _LOGGER.info(" + tinker_switch worked as intended")
696
+ else:
697
+ _LOGGER.info(" + tinker_switch failed unexpectedly")
698
+ return False
697
699
  except (
698
700
  pw_exceptions.ConnectionFailedError
699
701
  ): # leave for-loop at connect-error
@@ -706,6 +708,20 @@ class TestPlugwise: # pylint: disable=attribute-defined-outside-init
706
708
 
707
709
  return tinker_switch_passed
708
710
 
711
+ @pytest.mark.asyncio
712
+ async def tinker_switch_bad_input(
713
+ self, smile, dev_id=None, members=None, model="relay", unhappy=False
714
+ ):
715
+ """Enter a wrong state as input to toggle a Switch."""
716
+ _LOGGER.info("Test entering bad input set_switch_state:")
717
+ _LOGGER.info("- Devices (%s):", dev_id)
718
+ new_state = "false"
719
+ try:
720
+ await smile.set_switch_state(dev_id, members, model, new_state)
721
+ except pw_exceptions.PlugwiseError:
722
+ _LOGGER.info(" + failed input-check as expected")
723
+ return True # test is pass!
724
+
709
725
  @pytest.mark.asyncio
710
726
  async def tinker_thermostat_temp(
711
727
  self, smile, loc_id, block_cooling=False, fail_cooling=False, unhappy=False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes