Skip to content

Commit 1569338

Browse files
authored
Merge pull request #755 from plugwise/relay-sw-improve
Set_switch: don't raise eror when locked
2 parents ad53115 + da05900 commit 1569338

File tree

12 files changed

+245
-65
lines changed

12 files changed

+245
-65
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Changelog
22

3-
## Ongoing
3+
## v1.7.6
44

5-
- Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: 264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: 263](https://github.com/plugwise/python-plugwise-usb/pull/263)
5+
- Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: #264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: #263](https://github.com/plugwise/python-plugwise-usb/pull/263)
6+
- Don't raise an error when a locked switch is being toggled, and other switch-related improvements via [#755](https://github.com/plugwise/python-plugwise/pull/755)
67

78
## v1.7.5
89

fixtures/adam_multiple_devices_per_zone/data.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,19 @@
540540
"vendor": "Plugwise",
541541
"zigbee_mac_address": "ABCD012345670A11"
542542
},
543+
"e8ef2a01ed3b4139a53bf749204fe6b4": {
544+
"dev_class": "switching",
545+
"members": [
546+
"02cf28bfec924855854c544690a609ef",
547+
"4a810418d5394b3f82727340b91ba740"
548+
],
549+
"model": "Switchgroup",
550+
"name": "Test",
551+
"switches": {
552+
"relay": true
553+
},
554+
"vendor": "Plugwise"
555+
},
543556
"f1fee6043d3642a9b0a65297455f008e": {
544557
"available": true,
545558
"binary_sensors": {

fixtures/m_adam_multiple_devices_per_zone/data.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,19 @@
531531
"vendor": "Plugwise",
532532
"zigbee_mac_address": "ABCD012345670A11"
533533
},
534+
"e8ef2a01ed3b4139a53bf749204fe6b4": {
535+
"dev_class": "switching",
536+
"members": [
537+
"02cf28bfec924855854c544690a609ef",
538+
"4a810418d5394b3f82727340b91ba740"
539+
],
540+
"model": "Switchgroup",
541+
"name": "Test",
542+
"switches": {
543+
"relay": true
544+
},
545+
"vendor": "Plugwise"
546+
},
534547
"f1fee6043d3642a9b0a65297455f008e": {
535548
"available": true,
536549
"binary_sensors": {

plugwise/__init__.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
MODULES,
1616
NONE,
1717
SMILES,
18+
STATE_OFF,
19+
STATE_ON,
1820
STATUS,
1921
SYSTEM,
2022
GwEntityData,
@@ -398,10 +400,21 @@ async def set_temperature_offset(self, dev_id: str, offset: float) -> None:
398400

399401
async def set_switch_state(
400402
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+
403414
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+
)
405418
except ConnectionFailedError as exc:
406419
raise ConnectionFailedError(
407420
f"Failed to set switch state: {str(exc)}"

plugwise/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
PRESET_AWAY: Final = "away"
2424
PRESSURE_BAR: Final = "bar"
2525
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
26+
STATE_OFF: Final = "off"
27+
STATE_ON: Final = "on"
2628
TEMP_CELSIUS: Final = "°C"
2729
TEMP_KELVIN: Final = "°K"
2830
TIME_MILLISECONDS: Final = "ms"

plugwise/legacy/smile.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
OFF,
1919
REQUIRE_APPLIANCES,
2020
RULES,
21+
STATE_OFF,
22+
STATE_ON,
2123
GwEntityData,
2224
ThermoLoc,
2325
)
@@ -195,7 +197,7 @@ async def set_schedule_state(
195197
Determined from - DOMAIN_OBJECTS.
196198
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
197199
"""
198-
if state not in ("on", "off"):
200+
if state not in (STATE_OFF, STATE_ON):
199201
raise PlugwiseError("Plugwise: invalid schedule state.")
200202

201203
# Handle no schedule-name / Off-schedule provided
@@ -214,7 +216,7 @@ async def set_schedule_state(
214216
) # pragma: no cover
215217

216218
new_state = "false"
217-
if state == "on":
219+
if state == STATE_ON:
218220
new_state = "true"
219221

220222
locator = f'.//*[@id="{schedule_rule_id}"]/template'
@@ -234,13 +236,16 @@ async def set_schedule_state(
234236

235237
async def set_switch_state(
236238
self, appl_id: str, members: list[str] | None, model: str, state: str
237-
) -> None:
239+
) -> bool:
238240
"""Set the given state of the relevant switch.
239241
240242
For individual switches, sets the state directly.
241243
For group switches, sets the state for each member in the group separately.
242244
For switch-locks, sets the lock state using a different data format.
245+
Return the requested state when succesful, the current state otherwise.
243246
"""
247+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
248+
requested_state = state == STATE_ON
244249
switch = Munch()
245250
switch.actuator = "actuator_functionalities"
246251
switch.func_type = "relay_functionality"
@@ -250,7 +255,7 @@ async def set_switch_state(
250255

251256
# Handle switch-lock
252257
if model == "lock":
253-
state = "false" if state == "off" else "true"
258+
state = "true" if state == STATE_ON else "false"
254259
appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
255260
appl_name = appliance.find("name").text
256261
appl_type = appliance.find("type").text
@@ -269,37 +274,45 @@ async def set_switch_state(
269274
"</appliances>"
270275
)
271276
await self.call_request(APPLIANCES, method="post", data=data)
272-
return
277+
return requested_state
273278

274279
# Handle group of switches
275280
data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
276281
if members is not None:
277282
return await self._set_groupswitch_member_state(
278-
data, members, state, switch
283+
appl_id, data, members, state, switch
279284
)
280285

281286
# Handle individual relay switches
282287
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"]:
287289
# 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
290291

291292
await self.call_request(uri, method="put", data=data)
293+
return requested_state
292294

293295
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:
296298
"""Helper-function for set_switch_state().
297299
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.
299302
"""
303+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
304+
requested_state = state == STATE_ON
305+
switched = 0
300306
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
303316

304317
async def set_temperature(self, _: str, items: dict[str, float]) -> None:
305318
"""Set the given Temperature on the relevant Thermostat."""
@@ -310,7 +323,7 @@ async def set_temperature(self, _: str, items: dict[str, float]) -> None:
310323
if setpoint is None:
311324
raise PlugwiseError(
312325
"Plugwise: failed setting temperature: no valid input provided"
313-
) # pragma: no cover"
326+
) # pragma: no cover
314327

315328
temperature = str(setpoint)
316329
data = (

plugwise/smile.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from collections.abc import Awaitable, Callable
99
import datetime as dt
10-
from typing import Any
10+
from typing import Any, cast
1111

1212
from plugwise.constants import (
1313
ADAM,
@@ -22,7 +22,10 @@
2222
NOTIFICATIONS,
2323
OFF,
2424
RULES,
25+
STATE_OFF,
26+
STATE_ON,
2527
GwEntityData,
28+
SwitchType,
2629
ThermoLoc,
2730
)
2831
from plugwise.data import SmileData
@@ -309,12 +312,12 @@ async def set_schedule_state(
309312
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
310313
"""
311314
# Input checking
312-
if new_state not in ("on", "off"):
315+
if new_state not in (STATE_OFF, STATE_ON):
313316
raise PlugwiseError("Plugwise: invalid schedule state.")
314317

315318
# Translate selection of Off-schedule-option to disabling the active schedule
316319
if name == OFF:
317-
new_state = "off"
320+
new_state = STATE_OFF
318321

319322
# Handle no schedule-name / Off-schedule provided
320323
if name is None or name == OFF:
@@ -367,18 +370,27 @@ def determine_contexts(
367370
subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
368371
subject = etree.fromstring(subject)
369372

370-
if state == "off":
373+
if state == STATE_OFF:
371374
self._last_active[loc_id] = name
372375
contexts.remove(subject)
373-
if state == "on":
376+
if state == STATE_ON:
374377
contexts.append(subject)
375378

376379
return str(etree.tostring(contexts, encoding="unicode").rstrip())
377380

378381
async def set_switch_state(
379382
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
382394
switch = Munch()
383395
switch.actuator = "actuator_functionalities"
384396
switch.device = "relay"
@@ -396,10 +408,18 @@ async def set_switch_state(
396408

397409
if model == "lock":
398410
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+
)
400418

401419
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+
)
403423

404424
locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
405425
found = self._domain_objects.findall(locator)
@@ -412,39 +432,42 @@ async def set_switch_state(
412432
else: # actuators with a single item like relay_functionality
413433
switch_id = item.attrib["id"]
414434

415-
data = (
416-
f"<{switch.func_type}>"
417-
f"<{switch.func}>{state}</{switch.func}>"
418-
f"</{switch.func_type}>"
419-
)
420435
uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
421436
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
428442

429443
await self.call_request(uri, method="put", data=data)
444+
return requested_state
430445

431446
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:
434449
"""Helper-function for set_switch_state().
435450
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.
437453
"""
454+
current_state = self.gw_entities[appl_id]["switches"]["relay"]
455+
requested_state = state == STATE_ON
456+
switched = 0
438457
for member in members:
439458
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
440459
switch_id = self._domain_objects.find(locator).attrib["id"]
441460
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
448471

449472
async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
450473
"""Set the given Temperature on the relevant Thermostat."""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plugwise"
7-
version = "1.7.5"
7+
version = "1.7.6"
88
license = "MIT"
99
description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
1010
readme = "README.md"

tests/data/adam/adam_multiple_devices_per_zone.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,19 @@
567567
"vendor": "Plugwise",
568568
"zigbee_mac_address": "ABCD012345670A08"
569569
},
570+
"e8ef2a01ed3b4139a53bf749204fe6b4": {
571+
"dev_class": "switching",
572+
"members": [
573+
"02cf28bfec924855854c544690a609ef",
574+
"4a810418d5394b3f82727340b91ba740"
575+
],
576+
"model": "Switchgroup",
577+
"name": "Test",
578+
"switches": {
579+
"relay": true
580+
},
581+
"vendor": "Plugwise"
582+
},
570583
"fe799307f1624099878210aa0b9f1475": {
571584
"binary_sensors": {
572585
"plugwise_notification": true

0 commit comments

Comments
 (0)