Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7551d55
Let set-switch functions return a bool, improve handling of locked state
bouwew Jun 12, 2025
f89fe4e
Adapt tinker_switch() test function
bouwew Jun 12, 2025
46e05d6
fixup: relay-sw-improve Python code fixed using ruff
Jun 12, 2025
6a1634f
Return fixes
bouwew Jun 13, 2025
e3edfe9
Improve test
bouwew Jun 13, 2025
93d47b3
More return fixes
bouwew Jun 13, 2025
323c39a
Fix expected state value
bouwew Jun 13, 2025
0b2d616
Fix testcode
bouwew Jun 13, 2025
59c03a2
Fix legacy set_switch for lock
bouwew Jun 13, 2025
2eb36ba
Ruff fix
bouwew Jun 13, 2025
45b500f
Add testcase for a blocked switchgroup-change
bouwew Jun 13, 2025
68ed57c
Update related entity_items
bouwew Jun 13, 2025
e0f35ab
Save updates
bouwew Jun 13, 2025
75fae4f
Add pragma-no-cover for legacy
bouwew Jun 13, 2025
9f48e39
Test: clear no longer expected exception
bouwew Jun 13, 2025
a9abbe3
Bump to v1.7.6a0 test-version
bouwew Jun 13, 2025
93b9b2d
Correct switch state input
bouwew Jun 13, 2025
8a9085c
Bump to a1
bouwew Jun 13, 2025
31bc6a4
Improve var-name, correct returns
bouwew Jun 13, 2025
bae7297
Bump to a2
bouwew Jun 14, 2025
a25ee26
Implement improvements as suggested
bouwew Jun 15, 2025
8890065
Handle no lock-attribute present
bouwew Jun 15, 2025
0032a90
Correct testcase
bouwew Jun 15, 2025
ca3fd2c
Debug
bouwew Jun 15, 2025
806752a
Try
bouwew Jun 15, 2025
e4adde6
Ruff fixes
bouwew Jun 15, 2025
4cf708e
Clean up
bouwew Jun 15, 2025
e4c5825
Add testcase covering switching of lock
bouwew Jun 15, 2025
a7dc211
Ruff fix
bouwew Jun 15, 2025
8ae7acf
For legacy lock is always present
bouwew Jun 15, 2025
c6438ba
Mypy fix
bouwew Jun 15, 2025
9687d4e
Doc-string updates
bouwew Jun 15, 2025
aae4191
fixup: relay-sw-improve Python code fixed using ruff
Jun 15, 2025
a10fdae
Bump to a3
bouwew Jun 23, 2025
aa38035
Improve docstrings
bouwew Jun 23, 2025
c511870
Add input-check as suggested
bouwew Jun 23, 2025
b06106a
Move preset input-checking to __init__.py, use constants
bouwew Jun 23, 2025
9e80cfb
fixup: relay-sw-improve Python code fixed using ruff
Jun 23, 2025
e602444
Add missing constant imports
bouwew Jun 23, 2025
dca5835
Revert move preset input-checking
bouwew Jun 23, 2025
05d5300
Add tinker_switch_bad_input() test-function
bouwew Jun 23, 2025
089e156
And implement
bouwew Jun 23, 2025
2b0bf39
Fixes
bouwew Jun 23, 2025
9246b5d
Improve group-switch logic for Adam
bouwew Jun 23, 2025
73cf3ea
Update CHANGELOG
bouwew Jun 23, 2025
8b1fda1
Bump to v1.7.6 release-version
bouwew Jun 23, 2025
da05900
Use #issue
bouwew Jun 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog

## Ongoing
## v1.7.6

- 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)
- 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)
- 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)

## v1.7.5

Expand Down
13 changes: 13 additions & 0 deletions fixtures/adam_multiple_devices_per_zone/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,19 @@
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A11"
},
"e8ef2a01ed3b4139a53bf749204fe6b4": {
"dev_class": "switching",
"members": [
"02cf28bfec924855854c544690a609ef",
"4a810418d5394b3f82727340b91ba740"
],
"model": "Switchgroup",
"name": "Test",
"switches": {
"relay": true
},
"vendor": "Plugwise"
},
"f1fee6043d3642a9b0a65297455f008e": {
"available": true,
"binary_sensors": {
Expand Down
13 changes: 13 additions & 0 deletions fixtures/m_adam_multiple_devices_per_zone/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,19 @@
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A11"
},
"e8ef2a01ed3b4139a53bf749204fe6b4": {
"dev_class": "switching",
"members": [
"02cf28bfec924855854c544690a609ef",
"4a810418d5394b3f82727340b91ba740"
],
"model": "Switchgroup",
"name": "Test",
"switches": {
"relay": true
},
"vendor": "Plugwise"
},
"f1fee6043d3642a9b0a65297455f008e": {
"available": true,
"binary_sensors": {
Expand Down
19 changes: 16 additions & 3 deletions plugwise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
MODULES,
NONE,
SMILES,
STATE_OFF,
STATE_ON,
STATUS,
SYSTEM,
GwEntityData,
Expand Down Expand Up @@ -398,10 +400,21 @@ async def set_temperature_offset(self, dev_id: str, offset: float) -> None:

async def set_switch_state(
self, appl_id: str, members: list[str] | None, model: str, state: str
) -> None:
"""Set the given State of the relevant Switch."""
) -> bool:
"""Set the given State of the relevant Switch.

Return the result:
- True when switched to state on,
- False when switched to state off,
- the unchanged state when the switch is for instance locked.
"""
if state not in (STATE_OFF, STATE_ON):
raise PlugwiseError("Invalid state supplied to set_switch_state")

try:
await self._smile_api.set_switch_state(appl_id, members, model, state)
return await self._smile_api.set_switch_state(
appl_id, members, model, state
)
except ConnectionFailedError as exc:
raise ConnectionFailedError(
f"Failed to set switch state: {str(exc)}"
Expand Down
2 changes: 2 additions & 0 deletions plugwise/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
PRESET_AWAY: Final = "away"
PRESSURE_BAR: Final = "bar"
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
STATE_OFF: Final = "off"
STATE_ON: Final = "on"
TEMP_CELSIUS: Final = "°C"
TEMP_KELVIN: Final = "°K"
TIME_MILLISECONDS: Final = "ms"
Expand Down
49 changes: 31 additions & 18 deletions plugwise/legacy/smile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
OFF,
REQUIRE_APPLIANCES,
RULES,
STATE_OFF,
STATE_ON,
GwEntityData,
ThermoLoc,
)
Expand Down Expand Up @@ -195,7 +197,7 @@ async def set_schedule_state(
Determined from - DOMAIN_OBJECTS.
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
"""
if state not in ("on", "off"):
if state not in (STATE_OFF, STATE_ON):
raise PlugwiseError("Plugwise: invalid schedule state.")

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

new_state = "false"
if state == "on":
if state == STATE_ON:
new_state = "true"

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

async def set_switch_state(
self, appl_id: str, members: list[str] | None, model: str, state: str
) -> None:
) -> bool:
"""Set the given state of the relevant switch.

For individual switches, sets the state directly.
For group switches, sets the state for each member in the group separately.
For switch-locks, sets the lock state using a different data format.
Return the requested state when succesful, the current state otherwise.
"""
current_state = self.gw_entities[appl_id]["switches"]["relay"]
requested_state = state == STATE_ON
switch = Munch()
switch.actuator = "actuator_functionalities"
switch.func_type = "relay_functionality"
Expand All @@ -250,7 +255,7 @@ async def set_switch_state(

# Handle switch-lock
if model == "lock":
state = "false" if state == "off" else "true"
state = "true" if state == STATE_ON else "false"
appliance = self._appliances.find(f'appliance[@id="{appl_id}"]')
appl_name = appliance.find("name").text
appl_type = appliance.find("type").text
Expand All @@ -269,37 +274,45 @@ async def set_switch_state(
"</appliances>"
)
await self.call_request(APPLIANCES, method="post", data=data)
return
return requested_state

# Handle group of switches
data = f"<{switch.func_type}><state>{state}</state></{switch.func_type}>"
if members is not None:
return await self._set_groupswitch_member_state(
data, members, state, switch
appl_id, data, members, state, switch
)

# Handle individual relay switches
uri = f"{APPLIANCES};id={appl_id}/relay"
if model == "relay":
locator = (
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
)
if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]:
# Don't bother switching a relay when the corresponding lock-state is true
if self._appliances.find(locator).text == "true":
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
return current_state

await self.call_request(uri, method="put", data=data)
return requested_state

async def _set_groupswitch_member_state(
self, data: str, members: list[str], state: str, switch: Munch
) -> None:
self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
) -> bool:
"""Helper-function for set_switch_state().

Set the given State of the relevant Switch (relay) within a group of members.
Set the requested state of the relevant switch within a group of switches.
Return the current group-state when none of the switches has changed its state, the requested state otherwise.
"""
current_state = self.gw_entities[appl_id]["switches"]["relay"]
requested_state = state == STATE_ON
switched = 0
for member in members:
uri = f"{APPLIANCES};id={member}/relay"
await self.call_request(uri, method="put", data=data)
if not self.gw_entities[member]["switches"]["lock"]:
uri = f"{APPLIANCES};id={member}/relay"
await self.call_request(uri, method="put", data=data)
switched += 1

if switched > 0:
return requested_state

return current_state # pragma: no cover

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

temperature = str(setpoint)
data = (
Expand Down
81 changes: 52 additions & 29 deletions plugwise/smile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from collections.abc import Awaitable, Callable
import datetime as dt
from typing import Any
from typing import Any, cast

from plugwise.constants import (
ADAM,
Expand All @@ -22,7 +22,10 @@
NOTIFICATIONS,
OFF,
RULES,
STATE_OFF,
STATE_ON,
GwEntityData,
SwitchType,
ThermoLoc,
)
from plugwise.data import SmileData
Expand Down Expand Up @@ -309,12 +312,12 @@ async def set_schedule_state(
Used in HA Core to set the hvac_mode: in practice switch between schedule on - off.
"""
# Input checking
if new_state not in ("on", "off"):
if new_state not in (STATE_OFF, STATE_ON):
raise PlugwiseError("Plugwise: invalid schedule state.")

# Translate selection of Off-schedule-option to disabling the active schedule
if name == OFF:
new_state = "off"
new_state = STATE_OFF

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

if state == "off":
if state == STATE_OFF:
self._last_active[loc_id] = name
contexts.remove(subject)
if state == "on":
if state == STATE_ON:
contexts.append(subject)

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

async def set_switch_state(
self, appl_id: str, members: list[str] | None, model: str, state: str
) -> None:
"""Set the given State of the relevant Switch."""
) -> bool:
"""Set the given state of the relevant Switch.

For individual switches, sets the state directly.
For group switches, sets the state for each member in the group separately.
For switch-locks, sets the lock state using a different data format.
Return the requested state when succesful, the current state otherwise.
"""
model_type = cast(SwitchType, model)
current_state = self.gw_entities[appl_id]["switches"][model_type]
requested_state = state == STATE_ON
switch = Munch()
switch.actuator = "actuator_functionalities"
switch.device = "relay"
Expand All @@ -396,10 +408,18 @@ async def set_switch_state(

if model == "lock":
switch.func = "lock"
state = "false" if state == "off" else "true"
state = "true" if state == STATE_ON else "false"

data = (
f"<{switch.func_type}>"
f"<{switch.func}>{state}</{switch.func}>"
f"</{switch.func_type}>"
)

if members is not None:
return await self._set_groupswitch_member_state(members, state, switch)
return await self._set_groupswitch_member_state(
appl_id, data, members, state, switch
)

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

data = (
f"<{switch.func_type}>"
f"<{switch.func}>{state}</{switch.func}>"
f"</{switch.func_type}>"
)
uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
if model == "relay":
locator = (
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
)
# Don't bother switching a relay when the corresponding lock-state is true
if self._domain_objects.find(locator).text == "true":
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
lock_blocked = self.gw_entities[appl_id]["switches"].get("lock")
if lock_blocked or lock_blocked is None:
# Don't switch a relay when its corresponding lock-state is true or no
# lock is present. That means the relay can't be controlled by the user.
return current_state

await self.call_request(uri, method="put", data=data)
return requested_state

async def _set_groupswitch_member_state(
self, members: list[str], state: str, switch: Munch
) -> None:
self, appl_id: str, data: str, members: list[str], state: str, switch: Munch
) -> bool:
"""Helper-function for set_switch_state().

Set the given State of the relevant Switch within a group of members.
Set the requested state of the relevant switch within a group of switches.
Return the current group-state when none of the switches has changed its state, the requested state otherwise.
"""
current_state = self.gw_entities[appl_id]["switches"]["relay"]
requested_state = state == STATE_ON
switched = 0
for member in members:
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
switch_id = self._domain_objects.find(locator).attrib["id"]
uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
data = (
f"<{switch.func_type}>"
f"<{switch.func}>{state}</{switch.func}>"
f"</{switch.func_type}>"
)
await self.call_request(uri, method="put", data=data)
lock_blocked = self.gw_entities[member]["switches"].get("lock")
# Assume Plugs under Plugwise control are not part of a group
if lock_blocked is not None and not lock_blocked:
await self.call_request(uri, method="put", data=data)
switched += 1

if switched > 0:
return requested_state

return current_state

async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
"""Set the given Temperature on the relevant Thermostat."""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "plugwise"
version = "1.7.5"
version = "1.7.6"
license = "MIT"
description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
readme = "README.md"
Expand Down
13 changes: 13 additions & 0 deletions tests/data/adam/adam_multiple_devices_per_zone.json
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,19 @@
"vendor": "Plugwise",
"zigbee_mac_address": "ABCD012345670A08"
},
"e8ef2a01ed3b4139a53bf749204fe6b4": {
"dev_class": "switching",
"members": [
"02cf28bfec924855854c544690a609ef",
"4a810418d5394b3f82727340b91ba740"
],
"model": "Switchgroup",
"name": "Test",
"switches": {
"relay": true
},
"vendor": "Plugwise"
},
"fe799307f1624099878210aa0b9f1475": {
"binary_sensors": {
"plugwise_notification": true
Expand Down
Loading
Loading