Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## ongoing

- PR [327](https://github.com/plugwise/python-plugwise-usb/pull/327): Improve code quality according to SonarCloud, simplify sed awake timer

## v0.44.12 - 2025-08-24

- PR [323](https://github.com/plugwise/python-plugwise-usb/pull/323): Motion Sensitivity to use named levels (Off/Medium/High) instead of numeric values, add light sensitivity calibration on wake-up for scan devices.
Expand Down
51 changes: 18 additions & 33 deletions plugwise_usb/nodes/sed.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from asyncio import CancelledError, Future, Task, gather, get_running_loop, wait_for
from asyncio import Task, create_task, gather, sleep
from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import replace
from datetime import datetime, timedelta
Expand Down Expand Up @@ -85,7 +85,6 @@ def __init__(
):
"""Initialize base class for Sleeping End Device."""
super().__init__(mac, node_type, controller, loaded_callback)
self._loop = get_running_loop()
self._node_info.is_battery_powered = True

# Configure SED
Expand All @@ -95,7 +94,6 @@ def __init__(

self._last_awake: dict[NodeAwakeResponseType, datetime] = {}
self._last_awake_reason: str = "Unknown"
self._awake_future: Future[bool] | None = None

# Maintenance
self._maintenance_last_awake: datetime | None = None
Expand All @@ -118,10 +116,8 @@ async def load(self) -> bool:

async def unload(self) -> None:
"""Deactivate and unload node features."""
if self._awake_future is not None and not self._awake_future.done():
self._awake_future.set_result(True)
if self._awake_timer_task is not None and not self._awake_timer_task.done():
await self._awake_timer_task
self._awake_timer_task.cancel()
if self._awake_subscription is not None:
self._awake_subscription()
if self._delayed_task is not None and not self._delayed_task.done():
Expand Down Expand Up @@ -440,7 +436,7 @@ async def _awake_response(self, response: PlugwiseResponse) -> bool:
),
self.save_cache(),
]
self._delayed_task = self._loop.create_task(
self._delayed_task = create_task(
self._run_awake_tasks(), name=f"Delayed update for {self._mac_in_str}"
)
if response.awake_type == NodeAwakeResponseType.MAINTENANCE:
Expand Down Expand Up @@ -516,41 +512,30 @@ def _detect_maintenance_interval(self, timestamp: datetime) -> None:

async def _reset_awake(self, last_alive: datetime) -> None:
"""Reset node alive state."""
if self._awake_future is not None and not self._awake_future.done():
self._awake_future.set_result(True)
if self._awake_timer_task is not None and not self._awake_timer_task.done():
self._awake_timer_task.cancel()
# Setup new maintenance timer
self._awake_future = self._loop.create_future()
self._awake_timer_task = self._loop.create_task(
self._awake_timer_task = create_task(
self._awake_timer(), name=f"Node awake timer for {self._mac_in_str}"
)

async def _awake_timer(self) -> None:
"""Task to monitor to get next awake in time. If not it sets device to be unavailable."""
# wait for next maintenance timer, but allow missing one
if self._awake_future is None:
return
timeout_interval = self.maintenance_interval * 60 * 2.1
try:
await wait_for(
self._awake_future,
timeout=timeout_interval,
await sleep(timeout_interval)
# No maintenance awake message within expected time frame
# Mark node as unavailable
if self._available:
last_awake = self._last_awake.get(NodeAwakeResponseType.MAINTENANCE)
_LOGGER.warning(
"No awake message received from %s | last_maintenance_awake=%s | interval=%s (%s) | Marking node as unavailable",
self.name,
last_awake,
self.maintenance_interval,
timeout_interval,
)
except TimeoutError:
# No maintenance awake message within expected time frame
# Mark node as unavailable
if self._available:
last_awake = self._last_awake.get(NodeAwakeResponseType.MAINTENANCE)
_LOGGER.warning(
"No awake message received from %s | last_maintenance_awake=%s | interval=%s (%s) | Marking node as unavailable",
self.name,
last_awake,
self.maintenance_interval,
timeout_interval,
)
await self._available_update_state(False)
except CancelledError:
pass
self._awake_future = None
await self._available_update_state(False)

async def _run_awake_tasks(self) -> None:
"""Execute all awake tasks."""
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_usb"
version = "0.44.12"
version = "0.44.13a0"
license = "MIT"
keywords = ["home", "automation", "plugwise", "module", "usb"]
classifiers = [
Expand Down
30 changes: 17 additions & 13 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ async def test_stick_connect(self, monkeypatch: pytest.MonkeyPatch) -> None:
await stick.disconnect()
assert not stick.network_state
with pytest.raises(pw_exceptions.StickError):
assert stick.mac_stick
stick.mac_stick

async def disconnected(self, event: pw_api.StickEvent) -> None: # type: ignore[name-defined]
"""Handle disconnect event callback."""
Expand Down Expand Up @@ -611,17 +611,17 @@ async def test_stick_node_discovered_subscription( # noqa: PLR0915

# Check Scan is raising NodeError for unsupported features
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].relay
stick.nodes["5555555555555555"].relay
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].relay_state
stick.nodes["5555555555555555"].relay_state
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].switch
stick.nodes["5555555555555555"].switch
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].power
stick.nodes["5555555555555555"].power
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].sense
stick.nodes["5555555555555555"].sense
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["5555555555555555"].energy
stick.nodes["5555555555555555"].energy

# Motion
self.test_motion_on = asyncio.Future()
Expand Down Expand Up @@ -851,11 +851,11 @@ async def test_node_relay_and_power(self, monkeypatch: pytest.MonkeyPatch) -> No

# Check Circle is raising NodeError for unsupported features
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["0098765432101234"].motion
stick.nodes["0098765432101234"].motion
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["0098765432101234"].switch
stick.nodes["0098765432101234"].switch
with pytest.raises(pw_exceptions.FeatureError):
assert stick.nodes["0098765432101234"].sense
stick.nodes["0098765432101234"].sense

# Test relay init
# load node 2222222222222222 which has
Expand Down Expand Up @@ -931,7 +931,7 @@ async def fake_get_missing_energy_logs(address: int) -> None:
last_second=None, last_8_seconds=None, timestamp=None
)
pu = await stick.nodes["0098765432101234"].power_update()
assert pu.last_second == 21.2780505980402
assert pu.last_second == pytest.approx(21.2780505980402, rel=1e-09, abs=1e-09)
assert pu.last_8_seconds == -27.150578775440106

# Test energy state without request
Expand Down Expand Up @@ -1392,7 +1392,9 @@ def test_energy_counter(self) -> None:
0.07204743061527973,
reset_timestamp,
)
assert energy_counter_init.energy == 0.07204743061527973
assert energy_counter_init.energy == pytest.approx(
0.07204743061527973, rel=1e-09, abs=1e-09
)
assert energy_counter_init.last_reset == reset_timestamp
assert energy_counter_init.last_update == reset_timestamp + td(minutes=10)

Expand All @@ -1401,7 +1403,9 @@ def test_energy_counter(self) -> None:
0.08263379198066137,
reset_timestamp,
)
assert energy_counter_init.energy == 0.08263379198066137
assert energy_counter_init.energy == pytest.approx(
0.08263379198066137, rel=1e-09, abs=1e-09
)
assert energy_counter_init.last_reset == reset_timestamp
assert energy_counter_init.last_update == reset_timestamp + td(
minutes=15, seconds=10
Expand Down