diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4fb4d4d..3aa3b83f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index acf3b81cc..c980ea533 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -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 @@ -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 @@ -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 @@ -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(): @@ -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: @@ -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.""" diff --git a/pyproject.toml b/pyproject.toml index a90631a75..c8c5cd979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/tests/test_usb.py b/tests/test_usb.py index 7b20edc5e..be5b74e13 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -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.""" @@ -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() @@ -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 @@ -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 @@ -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) @@ -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