From 1f8d91d54467293da40de455957378aa555b14fd Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 14 Jul 2025 08:51:29 +0200 Subject: [PATCH 01/12] Propagate NodeType to node on init Unify loading of SED nodes Setup proper defaults if cache is missing or lacking and requests updates when nodes are awake Fix testing. --- plugwise_usb/nodes/__init__.py | 6 +++ plugwise_usb/nodes/circle.py | 4 +- plugwise_usb/nodes/node.py | 18 ++++----- plugwise_usb/nodes/scan.py | 69 +++++++++++++++++++++++----------- plugwise_usb/nodes/sed.py | 33 ++++++++-------- plugwise_usb/nodes/sense.py | 17 ++++++--- plugwise_usb/nodes/switch.py | 40 ++++++++++---------- tests/test_usb.py | 56 ++++++++++++++++++++------- 8 files changed, 155 insertions(+), 88 deletions(-) diff --git a/plugwise_usb/nodes/__init__.py b/plugwise_usb/nodes/__init__.py index 6022017f2..b8a12e5bf 100644 --- a/plugwise_usb/nodes/__init__.py +++ b/plugwise_usb/nodes/__init__.py @@ -26,6 +26,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseCirclePlus( mac, address, + node_type, controller, loaded_callback, ) @@ -33,6 +34,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseCircle( mac, address, + node_type, controller, loaded_callback, ) @@ -40,6 +42,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseSwitch( mac, address, + node_type, controller, loaded_callback, ) @@ -47,6 +50,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseSense( mac, address, + node_type, controller, loaded_callback, ) @@ -54,6 +58,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseScan( mac, address, + node_type, controller, loaded_callback, ) @@ -61,6 +66,7 @@ def get_plugwise_node( # noqa: PLR0911 return PlugwiseStealth( mac, address, + node_type, controller, loaded_callback, ) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index a7b9092a6..3da59f639 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -16,6 +16,7 @@ NodeFeature, NodeInfo, NodeInfoMessage, + NodeType, PowerStatistics, RelayConfig, RelayLock, @@ -81,11 +82,12 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize base class for Sleeping End Device.""" - super().__init__(mac, address, controller, loaded_callback) + super().__init__(mac, address, node_type, controller, loaded_callback) # Relay self._relay_lock: RelayLock = RelayLock() diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index bde5d949c..ce238a2e6 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -48,7 +48,6 @@ ) CACHE_FIRMWARE = "firmware" -CACHE_NODE_TYPE = "node_type" CACHE_HARDWARE = "hardware" CACHE_NODE_INFO_TIMESTAMP = "node_info_timestamp" CACHE_RELAY = "relay" @@ -61,16 +60,22 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize Plugwise base node class.""" super().__init__() + self.node_type = node_type self._loaded_callback = loaded_callback self._message_subscribe = controller.subscribe_to_messages self._features: tuple[NodeFeature, ...] = NODE_FEATURES self._last_seen = datetime.now(tz=UTC) - self._node_info = NodeInfo(mac, address) + self._node_info = NodeInfo( + mac=mac, + zigbee_address=address, + node_type=self.node_type, + ) self._ping = NetworkStatistics() self._mac_in_bytes = bytes(mac, encoding=UTF8) self._mac_in_str = mac @@ -462,7 +467,6 @@ async def node_info_update( _LOGGER.debug("No response for node_info_update() for %s", self.mac) await self._available_update_state(False) return self._node_info - await self._available_update_state(True, node_info.timestamp) await self.update_node_details(node_info) return self._node_info @@ -475,16 +479,13 @@ async def _node_info_load_from_cache(self) -> bool: firmware = self._get_cache_as_datetime(CACHE_FIRMWARE) hardware = self._get_cache(CACHE_HARDWARE) - node_type: NodeType | None = None - if (node_type_str := self._get_cache(CACHE_NODE_TYPE)) is not None: - node_type = NodeType(int(node_type_str)) relay_state = self._get_cache(CACHE_RELAY) == "True" timestamp = self._get_cache_as_datetime(CACHE_NODE_INFO_TIMESTAMP) node_info = NodeInfoMessage( current_logaddress_pointer=None, firmware=firmware, hardware=hardware, - node_type=node_type, + node_type=self.node_type, relay_state=relay_state, timestamp=timestamp, ) @@ -509,9 +510,6 @@ async def update_node_details( complete = True if node_info.node_type is None: complete = False - else: - self._node_info.node_type = NodeType(node_info.node_type) - self._set_cache(CACHE_NODE_TYPE, self._node_info.node_type.value) if node_info.firmware is None: complete = False diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index db65d550b..20ad24c51 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -9,7 +9,14 @@ import logging from typing import Any, Final -from ..api import MotionConfig, MotionSensitivity, MotionState, NodeEvent, NodeFeature +from ..api import ( + MotionConfig, + MotionSensitivity, + MotionState, + NodeEvent, + NodeFeature, + NodeType, +) from ..connection import StickController from ..constants import MAX_UINT_2 from ..exceptions import MessageError, NodeError, NodeTimeout @@ -48,11 +55,20 @@ # Light override SCAN_DEFAULT_DAYLIGHT_MODE: Final = False +# Default firmware if not known +DEFAULT_FIRMWARE: Final = datetime(2010, 11, 4, 16, 58, 46, tzinfo=UTC) + # Sensitivity values for motion sensor configuration SENSITIVITY_HIGH_VALUE = 20 # 0x14 SENSITIVITY_MEDIUM_VALUE = 30 # 0x1E SENSITIVITY_OFF_VALUE = 255 # 0xFF +# Scan Features +SCAN_FEATURES: Final = ( + NodeFeature.MOTION, + NodeFeature.MOTION_CONFIG, +) + # endregion @@ -63,11 +79,12 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize Scan Device.""" - super().__init__(mac, address, controller, loaded_callback) + super().__init__(mac, address, node_type, controller, loaded_callback) self._unsubscribe_switch_group: Callable[[], None] | None = None self._reset_timer_motion_on: datetime | None = None self._scan_subscription: Callable[[], None] | None = None @@ -89,26 +106,17 @@ async def load(self) -> bool: """Load and activate Scan node features.""" if self._loaded: return True - if self._cache_enabled: - _LOGGER.debug("Load Scan node %s from cache", self._node_info.mac) - await self._load_from_cache() - else: - self._load_defaults() - self._loaded = True - self._setup_protocol( - SCAN_FIRMWARE_SUPPORT, - ( - NodeFeature.BATTERY, - NodeFeature.INFO, - NodeFeature.PING, - NodeFeature.MOTION, - NodeFeature.MOTION_CONFIG, - ), - ) + + _LOGGER.debug("Loading Scan node %s", self._node_info.mac) + if not await super().load(): + _LOGGER.warning("Load Scan base node failed") + return False + + self._setup_protocol(SCAN_FIRMWARE_SUPPORT, SCAN_FEATURES) if await self.initialize(): await self._loaded_callback(NodeEvent.LOADED, self.mac) return True - _LOGGER.debug("Load of Scan node %s failed", self._node_info.mac) + _LOGGER.warning("Load Scan node %s failed", self._node_info.mac) return False @raise_not_loaded @@ -132,9 +140,9 @@ async def unload(self) -> None: await super().unload() # region Caching - def _load_defaults(self) -> None: + async def _load_defaults(self) -> None: """Load default configuration settings.""" - super()._load_defaults() + await super()._load_defaults() self._motion_state = MotionState( state=SCAN_DEFAULT_MOTION_STATE, timestamp=None, @@ -144,11 +152,28 @@ def _load_defaults(self) -> None: daylight_mode=SCAN_DEFAULT_DAYLIGHT_MODE, sensitivity_level=SCAN_DEFAULT_SENSITIVITY, ) + if self._node_info.model is None: + self._node_info.model = "Scan" + self.node_info_default = True + if self._node_info.name is None: + self._node_info.name = f"Scan {self._node_info.mac[-5:]}" + self.node_info_default = True + if self._node_info.name is None: + self._node_info.name = f"Scan {self._node_info.mac[-5:]}" + self.node_info_default = True + if self._node_info.firmware is None: + self._node_info.firmware = DEFAULT_FIRMWARE + self.node_info_default = True + self._new_reset_timer = SCAN_DEFAULT_MOTION_RESET_TIMER + self._new_daylight_mode = SCAN_DEFAULT_DAYLIGHT_MODE + self._new_sensitivity_level = SCAN_DEFAULT_SENSITIVITY + await self.schedule_task_when_awake(self._configure_scan_task()) + self._scan_config_task_scheduled = True async def _load_from_cache(self) -> bool: """Load states from previous cached information. Returns True if successful.""" if not await super()._load_from_cache(): - self._load_defaults() + await self._load_defaults() return False self._motion_state = MotionState( state=self._motion_from_cache(), diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index e45849c5a..2a48924c8 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -17,7 +17,7 @@ import logging from typing import Any, Final -from ..api import BatteryConfig, NodeEvent, NodeFeature, NodeInfo +from ..api import BatteryConfig, NodeEvent, NodeFeature, NodeInfo, NodeType from ..connection import StickController from ..constants import MAX_UINT_2, MAX_UINT_4 from ..exceptions import MessageError, NodeError @@ -64,6 +64,12 @@ # Time in minutes the SED will sleep SED_DEFAULT_SLEEP_DURATION: Final = 60 +# Default firmware if not known +DEFAULT_FIRMWARE: Final = None + +# SED BaseNode Features +SED_FEATURES: Final = (NodeFeature.BATTERY,) + # Value limits MAX_MINUTE_INTERVAL: Final = 1440 @@ -83,11 +89,12 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize base class for Sleeping End Device.""" - super().__init__(mac, address, controller, loaded_callback) + super().__init__(mac, address, node_type, controller, loaded_callback) self._loop = get_running_loop() self._node_info.is_battery_powered = True @@ -117,12 +124,8 @@ async def load(self) -> bool: else: self._load_defaults() self._loaded = True - self._features += (NodeFeature.BATTERY,) - if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) - return True - _LOGGER.debug("Load of SED node %s failed", self._node_info.mac) - return False + self._features += SED_FEATURES + return True async def unload(self) -> None: """Deactivate and unload node features.""" @@ -156,7 +159,7 @@ async def initialize(self) -> bool: await super().initialize() return True - def _load_defaults(self) -> None: + async def _load_defaults(self) -> None: """Load default configuration settings.""" self._battery_config = BatteryConfig( awake_duration=SED_DEFAULT_AWAKE_DURATION, @@ -165,11 +168,15 @@ def _load_defaults(self) -> None: maintenance_interval=SED_DEFAULT_MAINTENANCE_INTERVAL, sleep_duration=SED_DEFAULT_SLEEP_DURATION, ) + await self.schedule_task_when_awake(self.node_info_update(None)) + self._sed_config_task_scheduled = True + self._new_battery_config = self._battery_config + await self.schedule_task_when_awake(self._configure_sed_task()) async def _load_from_cache(self) -> bool: """Load states from previous cached information. Returns True if successful.""" if not await super()._load_from_cache(): - self._load_defaults() + await self._load_defaults() return False self._battery_config = BatteryConfig( awake_duration=self._awake_duration_from_cache(), @@ -240,9 +247,6 @@ async def set_awake_duration(self, seconds: int) -> bool: f"Invalid awake duration ({seconds}). It must be between 1 and 255 seconds." ) - if self._battery_config.awake_duration == seconds: - return False - self._new_battery_config = replace( self._new_battery_config, awake_duration=seconds ) @@ -491,7 +495,7 @@ async def node_info_update( self, node_info: NodeInfoResponse | None = None ) -> NodeInfo | None: """Update Node (hardware) information.""" - if node_info is None and self.skip_update(self._node_info, 86400): + if node_info is not None and self.skip_update(self._node_info, 86400): return self._node_info return await super().node_info_update(node_info) @@ -643,7 +647,6 @@ async def _send_tasks(self) -> None: """Send all tasks in queue.""" if len(self._send_task_queue) == 0: return - async with self._send_task_lock: task_result = await gather(*self._send_task_queue) if not all(task_result): diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index dad2dd37d..5eed8c536 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -3,10 +3,11 @@ from __future__ import annotations from collections.abc import Awaitable, Callable +from datetime import UTC, datetime import logging from typing import Any, Final -from ..api import NodeEvent, NodeFeature, SenseStatistics +from ..api import NodeEvent, NodeFeature, NodeType, SenseStatistics from ..connection import StickController from ..exceptions import MessageError, NodeError from ..messages.responses import SENSE_REPORT_ID, PlugwiseResponse, SenseReportResponse @@ -30,6 +31,9 @@ NodeFeature.SENSE, ) +# Default firmware if not known +DEFAULT_FIRMWARE: Final = datetime(2010, 12, 3, 10, 17, 7, tzinfo=UTC) + class PlugwiseSense(NodeSED): """Plugwise Sense node.""" @@ -38,11 +42,12 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize Scan Device.""" - super().__init__(mac, address, controller, loaded_callback) + super().__init__(mac, address, node_type, controller, loaded_callback) self._sense_statistics = SenseStatistics() @@ -55,7 +60,7 @@ async def load(self) -> bool: _LOGGER.debug("Loading Sense node %s", self._node_info.mac) if not await super().load(): - _LOGGER.debug("Load Sense base node failed") + _LOGGER.warning("Load Sense base node failed") return False self._setup_protocol(SENSE_FIRMWARE_SUPPORT, SENSE_FEATURES) @@ -63,7 +68,7 @@ async def load(self) -> bool: await self._loaded_callback(NodeEvent.LOADED, self.mac) return True - _LOGGER.debug("Load Sense node %s failed", self._node_info.mac) + _LOGGER.warning("Load Sense node %s failed", self._node_info.mac) return False @raise_not_loaded @@ -87,9 +92,9 @@ async def unload(self) -> None: self._sense_subscription() await super().unload() - def _load_defaults(self) -> None: + async def _load_defaults(self) -> None: """Load default configuration settings.""" - super()._load_defaults() + await super()._load_defaults() self._sense_statistics = SenseStatistics( temperature=0.0, humidity=0.0, diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index a0f0edec2..8cdf930a9 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -5,11 +5,11 @@ from asyncio import gather from collections.abc import Awaitable, Callable from dataclasses import replace -from datetime import datetime +from datetime import UTC, datetime import logging -from typing import Any +from typing import Any, Final -from ..api import NodeEvent, NodeFeature, SwitchGroup +from ..api import NodeEvent, NodeFeature, NodeType, SwitchGroup from ..connection import StickController from ..exceptions import MessageError, NodeError from ..messages.responses import ( @@ -23,6 +23,12 @@ _LOGGER = logging.getLogger(__name__) +# Switch Features +SWITCH_FEATURES: Final = (NodeFeature.SWITCH,) + +# Default firmware if not known +DEFAULT_FIRMWARE: Final = datetime(2009, 9, 8, 14, 7, 4, tzinfo=UTC) + class PlugwiseSwitch(NodeSED): """Plugwise Switch node.""" @@ -31,11 +37,12 @@ def __init__( self, mac: str, address: int, + node_type: NodeType, controller: StickController, loaded_callback: Callable[[NodeEvent, str], Awaitable[None]], ): """Initialize Scan Device.""" - super().__init__(mac, address, controller, loaded_callback) + super().__init__(mac, address, node_type, controller, loaded_callback) self._switch_subscription: Callable[[], None] | None = None self._switch = SwitchGroup() @@ -43,25 +50,18 @@ async def load(self) -> bool: """Load and activate Switch node features.""" if self._loaded: return True - if self._cache_enabled: - _LOGGER.debug("Load Switch node %s from cache", self._node_info.mac) - await self._load_from_cache() - else: - self._load_defaults() - self._loaded = True - self._setup_protocol( - SWITCH_FIRMWARE_SUPPORT, - ( - NodeFeature.BATTERY, - NodeFeature.INFO, - NodeFeature.PING, - NodeFeature.SWITCH, - ), - ) + + _LOGGER.debug("Loading Switch node %s", self._node_info.mac) + if not await super().load(): + _LOGGER.warning("Load Switch base node failed") + return False + + self._setup_protocol(SWITCH_FIRMWARE_SUPPORT, SWITCH_FEATURES) if await self.initialize(): await self._loaded_callback(NodeEvent.LOADED, self.mac) return True - _LOGGER.debug("Load of Switch node %s failed", self._node_info.mac) + + _LOGGER.warning("Load Switch node %s failed", self._node_info.mac) return False @raise_not_loaded diff --git a/tests/test_usb.py b/tests/test_usb.py index 187d24cbf..e9383ddc5 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1788,7 +1788,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign """Load callback for event.""" test_node = pw_sed.PlugwiseBaseNode( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.CIRCLE, + mock_stick_controller, + load_callback, ) # Validate base node properties which are always set @@ -1927,7 +1931,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign """Load callback for event.""" test_sed = pw_sed.NodeSED( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.SCAN, + mock_stick_controller, + load_callback, ) assert not test_sed.cache_enabled @@ -1969,8 +1977,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_sed.set_awake_duration(0) with pytest.raises(ValueError): assert await test_sed.set_awake_duration(256) - assert not await test_sed.set_awake_duration(10) - assert not test_sed.sed_config_task_scheduled + assert await test_sed.set_awake_duration(10) + assert test_sed.sed_config_task_scheduled assert await test_sed.set_awake_duration(15) assert test_sed.sed_config_task_scheduled assert test_sed.battery_config.awake_duration == 15 @@ -2103,8 +2111,8 @@ def fake_cache(dummy: object, setting: str) -> str | None: # noqa: PLR0911 PLR0 return "2011-6-27-8-55-44" if setting == pw_node.CACHE_HARDWARE: return "080007" - if setting == pw_node.CACHE_NODE_TYPE: - return "6" + if setting == pw_node.CACHE_RELAY: + return "True" if setting == pw_node.CACHE_NODE_INFO_TIMESTAMP: return "2024-12-7-1-0-0" if setting == pw_sed.CACHE_AWAKE_DURATION: @@ -2145,7 +2153,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign """Load callback for event.""" test_scan = pw_scan.PlugwiseScan( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.SCAN, + mock_stick_controller, + load_callback, ) assert not test_scan.cache_enabled node_info = pw_api.NodeInfoMessage( @@ -2165,8 +2177,8 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_scan.set_motion_reset_timer(0) with pytest.raises(ValueError): assert await test_scan.set_motion_reset_timer(256) - assert not await test_scan.set_motion_reset_timer(10) - assert not test_scan.scan_config_task_scheduled + assert await test_scan.set_motion_reset_timer(10) + assert test_scan.scan_config_task_scheduled assert await test_scan.set_motion_reset_timer(15) assert test_scan.scan_config_task_scheduled assert test_scan.reset_timer == 15 @@ -2250,7 +2262,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign # scan with cache enabled mock_stick_controller.send_response = None test_scan = pw_scan.PlugwiseScan( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.SCAN, + mock_stick_controller, + load_callback, ) node_info = pw_api.NodeInfoMessage( current_logaddress_pointer=None, @@ -2294,10 +2310,10 @@ def fake_cache(dummy: object, setting: str) -> str | None: # noqa: PLR0911 return "2011-5-13-7-26-54" if setting == pw_node.CACHE_HARDWARE: return "080029" - if setting == pw_node.CACHE_NODE_TYPE: - return "3" if setting == pw_node.CACHE_NODE_INFO_TIMESTAMP: return "2024-12-7-1-0-0" + if setting == pw_node.CACHE_RELAY: + return "True" if setting == pw_sed.CACHE_AWAKE_DURATION: return "15" if setting == pw_sed.CACHE_CLOCK_INTERVAL: @@ -2317,7 +2333,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign """Load callback for event.""" test_switch = pw_switch.PlugwiseSwitch( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.SWITCH, + mock_stick_controller, + load_callback, ) assert not test_switch.cache_enabled @@ -2343,7 +2363,11 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign # switch with cache enabled test_switch = pw_switch.PlugwiseSwitch( - "1298347650AFBECD", 1, mock_stick_controller, load_callback + "1298347650AFBECD", + 1, + pw_api.NodeType.SWITCH, + mock_stick_controller, + load_callback, ) node_info = pw_api.NodeInfoMessage( current_logaddress_pointer=None, @@ -2626,6 +2650,10 @@ async def test_node_discovery_and_load( # noqa: PLR0915 assert stick.nodes["8888888888888888"].node_info.model_type is None assert stick.nodes["8888888888888888"].available assert stick.nodes["8888888888888888"].node_info.is_battery_powered + assert ( + stick.nodes["8888888888888888"].node_info.node_type + == pw_api.NodeType.SWITCH + ) assert sorted(stick.nodes["8888888888888888"].features) == sorted( ( pw_api.NodeFeature.AVAILABLE, From a8daf035356bbde157ac1f55e8f7740c9f3eca00 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 15 Jul 2025 14:39:11 +0200 Subject: [PATCH 02/12] remove remaing node_info_default and double name definition --- plugwise_usb/nodes/scan.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 20ad24c51..a4eb914d2 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -154,16 +154,10 @@ async def _load_defaults(self) -> None: ) if self._node_info.model is None: self._node_info.model = "Scan" - self.node_info_default = True if self._node_info.name is None: self._node_info.name = f"Scan {self._node_info.mac[-5:]}" - self.node_info_default = True - if self._node_info.name is None: - self._node_info.name = f"Scan {self._node_info.mac[-5:]}" - self.node_info_default = True if self._node_info.firmware is None: self._node_info.firmware = DEFAULT_FIRMWARE - self.node_info_default = True self._new_reset_timer = SCAN_DEFAULT_MOTION_RESET_TIMER self._new_daylight_mode = SCAN_DEFAULT_DAYLIGHT_MODE self._new_sensitivity_level = SCAN_DEFAULT_SENSITIVITY @@ -555,7 +549,7 @@ async def _scan_configure_update( daylight_mode=daylight_mode, ) self._set_cache(CACHE_MOTION_RESET_TIMER, str(motion_reset_timer)) - self._set_cache(CACHE_SCAN_SENSITIVITY, sensitivity_level.value) + self._set_cache(CACHE_SCAN_SENSITIVITY, sensitivity_level) if daylight_mode: self._set_cache(CACHE_SCAN_DAYLIGHT_MODE, "True") else: From 085352058849eb8f57be64ea9e67c1e5cc4f8b2e Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 09:08:49 +0200 Subject: [PATCH 03/12] Remove double load_defaults calls. Improve fallback to load_defaults in SED --- plugwise_usb/nodes/scan.py | 1 - plugwise_usb/nodes/sed.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index a4eb914d2..e93ddcb44 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -167,7 +167,6 @@ async def _load_defaults(self) -> None: async def _load_from_cache(self) -> bool: """Load states from previous cached information. Returns True if successful.""" if not await super()._load_from_cache(): - await self._load_defaults() return False self._motion_state = MotionState( state=self._motion_from_cache(), diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 2a48924c8..44fa048b2 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -118,10 +118,11 @@ async def load(self) -> bool: """Load and activate SED node features.""" if self._loaded: return True - if self._cache_enabled: - _LOGGER.debug("Load SED node %s from cache", self._node_info.mac) - await self._load_from_cache() - else: + _LOGGER.debug("Load SED node %s from cache", self._node_info.mac) + if await self._load_from_cache(): + self._loaded = True + if not self._loaded: + _LOGGER.debug("Load SED node %s defaults", self._node_info.mac) self._load_defaults() self._loaded = True self._features += SED_FEATURES @@ -176,7 +177,6 @@ async def _load_defaults(self) -> None: async def _load_from_cache(self) -> bool: """Load states from previous cached information. Returns True if successful.""" if not await super()._load_from_cache(): - await self._load_defaults() return False self._battery_config = BatteryConfig( awake_duration=self._awake_duration_from_cache(), From 2af5d3eca6f9ad45a472e4fa6ede04cd52a3a2a2 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 09:17:59 +0200 Subject: [PATCH 04/12] Extend default values loading to Sense and Switch --- plugwise_usb/nodes/sense.py | 9 +++++++++ plugwise_usb/nodes/switch.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 5eed8c536..2f554597d 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -92,6 +92,7 @@ async def unload(self) -> None: self._sense_subscription() await super().unload() + # region Caching async def _load_defaults(self) -> None: """Load default configuration settings.""" await super()._load_defaults() @@ -99,6 +100,14 @@ async def _load_defaults(self) -> None: temperature=0.0, humidity=0.0, ) + if self._node_info.model is None: + self._node_info.model = "Sense" + if self._node_info.name is None: + self._node_info.name = f"Sense {self._node_info.mac[-5:]}" + if self._node_info.firmware is None: + self._node_info.firmware = DEFAULT_FIRMWARE + + # endregion # region properties diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 8cdf930a9..2f39e330c 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -84,6 +84,19 @@ async def unload(self) -> None: self._switch_subscription() await super().unload() + # region Caching + async def _load_defaults(self) -> None: + """Load default configuration settings.""" + await super()._load_defaults() + if self._node_info.model is None: + self._node_info.model = "Switch" + if self._node_info.name is None: + self._node_info.name = f"Switch {self._node_info.mac[-5:]}" + if self._node_info.firmware is None: + self._node_info.firmware = DEFAULT_FIRMWARE + + # endregion + # region Properties @property From 726dcddea4a7438399d1edcbc0f5fa8d7c875421 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 11:04:57 +0200 Subject: [PATCH 05/12] Let SED->load() return None --- plugwise_usb/nodes/scan.py | 4 +--- plugwise_usb/nodes/sed.py | 3 +-- plugwise_usb/nodes/sense.py | 4 +--- plugwise_usb/nodes/switch.py | 4 +--- tests/test_usb.py | 2 +- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index e93ddcb44..749a6e921 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -108,9 +108,7 @@ async def load(self) -> bool: return True _LOGGER.debug("Loading Scan node %s", self._node_info.mac) - if not await super().load(): - _LOGGER.warning("Load Scan base node failed") - return False + await super().load() self._setup_protocol(SCAN_FIRMWARE_SUPPORT, SCAN_FEATURES) if await self.initialize(): diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 44fa048b2..24900c525 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -114,7 +114,7 @@ def __init__( self._maintenance_last_awake: datetime | None = None self._maintenance_interval_restored_from_cache = False - async def load(self) -> bool: + async def load(self) -> None: """Load and activate SED node features.""" if self._loaded: return True @@ -126,7 +126,6 @@ async def load(self) -> bool: self._load_defaults() self._loaded = True self._features += SED_FEATURES - return True async def unload(self) -> None: """Deactivate and unload node features.""" diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 2f554597d..4b6615164 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -59,9 +59,7 @@ async def load(self) -> bool: return True _LOGGER.debug("Loading Sense node %s", self._node_info.mac) - if not await super().load(): - _LOGGER.warning("Load Sense base node failed") - return False + await super().load() self._setup_protocol(SENSE_FIRMWARE_SUPPORT, SENSE_FEATURES) if await self.initialize(): diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 2f39e330c..62695d5ff 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -52,9 +52,7 @@ async def load(self) -> bool: return True _LOGGER.debug("Loading Switch node %s", self._node_info.mac) - if not await super().load(): - _LOGGER.warning("Load Switch base node failed") - return False + await super().load() self._setup_protocol(SWITCH_FIRMWARE_SUPPORT, SWITCH_FEATURES) if await self.initialize(): diff --git a/tests/test_usb.py b/tests/test_usb.py index e9383ddc5..2127b7545 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1951,7 +1951,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sed.node_info.is_battery_powered assert test_sed.is_battery_powered - assert await test_sed.load() + assert await test_sed.load() is None assert sorted(test_sed.features) == sorted( ( pw_api.NodeFeature.AVAILABLE, From 6416abc070ac385c43ab2b93dffe0ee504ac8cfa Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 11:09:52 +0200 Subject: [PATCH 06/12] Fix return value --- plugwise_usb/nodes/sed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 24900c525..446ec9e20 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -117,7 +117,7 @@ def __init__( async def load(self) -> None: """Load and activate SED node features.""" if self._loaded: - return True + return _LOGGER.debug("Load SED node %s from cache", self._node_info.mac) if await self._load_from_cache(): self._loaded = True From 076ff5bd188f2daad4b36386ac1dc18baece2d53 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 15:28:53 +0200 Subject: [PATCH 07/12] await _load_defaults --- plugwise_usb/nodes/sed.py | 2 +- tests/test_usb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 446ec9e20..1fdb1d321 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -123,7 +123,7 @@ async def load(self) -> None: self._loaded = True if not self._loaded: _LOGGER.debug("Load SED node %s defaults", self._node_info.mac) - self._load_defaults() + await self._load_defaults() self._loaded = True self._features += SED_FEATURES diff --git a/tests/test_usb.py b/tests/test_usb.py index 2127b7545..2de50f097 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2177,7 +2177,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_scan.set_motion_reset_timer(0) with pytest.raises(ValueError): assert await test_scan.set_motion_reset_timer(256) - assert await test_scan.set_motion_reset_timer(10) + assert not await test_scan.set_motion_reset_timer(10) assert test_scan.scan_config_task_scheduled assert await test_scan.set_motion_reset_timer(15) assert test_scan.scan_config_task_scheduled From 2efa143dc10300a6484ee2081f4ea0a0ec05a607 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 18:59:09 +0200 Subject: [PATCH 08/12] Change load() to return None in all cases --- plugwise_usb/network/__init__.py | 7 ++-- plugwise_usb/nodes/circle.py | 58 ++++++++++++++++++-------------- plugwise_usb/nodes/scan.py | 16 ++++----- plugwise_usb/nodes/sed.py | 11 +++--- plugwise_usb/nodes/sense.py | 17 ++++------ plugwise_usb/nodes/switch.py | 17 ++++------ tests/test_usb.py | 12 +++---- 7 files changed, 65 insertions(+), 73 deletions(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 3d05f20b0..660821085 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -481,10 +481,9 @@ async def _load_node(self, mac: str) -> bool: return False if self._nodes[mac].is_loaded: return True - if await self._nodes[mac].load(): - await self._notify_node_event_subscribers(NodeEvent.LOADED, mac) - return True - return False + await self._nodes[mac].load() + await self._notify_node_event_subscribers(NodeEvent.LOADED, mac) + return True async def _load_stragglers(self) -> None: """Retry failed load operation.""" diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 3da59f639..79e2785fe 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -8,7 +8,7 @@ from datetime import UTC, datetime from functools import wraps import logging -from typing import Any, TypeVar, cast +from typing import Any, Final, TypeVar, cast from ..api import ( EnergyStatistics, @@ -59,6 +59,19 @@ CACHE_RELAY_INIT = "relay_init" CACHE_RELAY_LOCK = "relay_lock" +CIRCLE_FEATURES: Final = ( + NodeFeature.CIRCLE, + NodeFeature.RELAY, + NodeFeature.RELAY_INIT, + NodeFeature.RELAY_LOCK, + NodeFeature.ENERGY, + NodeFeature.POWER, +) + + +# Default firmware if not known +DEFAULT_FIRMWARE: Final = datetime(2008, 8, 26, 15, 46, tzinfo=UTC) + FuncT = TypeVar("FuncT", bound=Callable[..., Any]) _LOGGER = logging.getLogger(__name__) @@ -884,10 +897,10 @@ async def clock_synchronize(self) -> bool: return True return False - async def load(self) -> bool: + async def load(self) -> None: """Load and activate Circle node features.""" if self._loaded: - return True + return if self._cache_enabled: _LOGGER.debug("Loading Circle node %s from cache", self._mac_in_str) @@ -897,34 +910,18 @@ async def load(self) -> bool: _LOGGER.debug("Retrieving Info For Circle node %s", self._mac_in_str) # Check if node is online - if not self._available and not await self.is_online(): - _LOGGER.debug( - "Failed to load Circle node %s because it is not online", - self._mac_in_str, - ) - return False - - # Get node info - if await self.node_info_update() is None: + if ( + not self._available and not await self.is_online() + ) or await self.node_info_update() is None: _LOGGER.debug( - "Failed to load Circle node %s because it is not responding to information request", + "Failed to retrieve NodeInfo for %s, loading defaults", self._mac_in_str, ) - return False + await self._load_defaults() - self._loaded = True + self._loaded = True - self._setup_protocol( - CIRCLE_FIRMWARE_SUPPORT, - ( - NodeFeature.CIRCLE, - NodeFeature.RELAY, - NodeFeature.RELAY_INIT, - NodeFeature.RELAY_LOCK, - NodeFeature.ENERGY, - NodeFeature.POWER, - ), - ) + self._setup_protocol(CIRCLE_FIRMWARE_SUPPORT, CIRCLE_FEATURES) await self._loaded_callback(NodeEvent.LOADED, self.mac) await self.initialize() return True @@ -974,6 +971,15 @@ async def _load_from_cache(self) -> bool: return result + async def _load_defaults(self) -> None: + """Load default configuration settings.""" + if self._node_info.model is None: + self._node_info.model = "Circle" + if self._node_info.name is None: + self._node_info.name = f"Circle {self._node_info.mac[-5:]}" + if self._node_info.firmware is None: + self._node_info.firmware = DEFAULT_FIRMWARE + @raise_not_loaded async def initialize(self) -> bool: """Initialize node.""" diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 749a6e921..775488363 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -102,26 +102,23 @@ def __init__( # region Load & Initialize - async def load(self) -> bool: + async def load(self) -> None: """Load and activate Scan node features.""" if self._loaded: - return True + return _LOGGER.debug("Loading Scan node %s", self._node_info.mac) await super().load() self._setup_protocol(SCAN_FIRMWARE_SUPPORT, SCAN_FEATURES) - if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) - return True - _LOGGER.warning("Load Scan node %s failed", self._node_info.mac) - return False + await self.initialize() + await self._loaded_callback(NodeEvent.LOADED, self.mac) @raise_not_loaded - async def initialize(self) -> bool: + async def initialize(self) -> None: """Initialize Scan node.""" if self._initialized: - return True + return self._unsubscribe_switch_group = await self._message_subscribe( self._switch_group, @@ -129,7 +126,6 @@ async def initialize(self) -> bool: (NODE_SWITCH_GROUP_ID,), ) await super().initialize() - return True async def unload(self) -> None: """Unload node.""" diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 1fdb1d321..d454a76c6 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -114,10 +114,11 @@ def __init__( self._maintenance_last_awake: datetime | None = None self._maintenance_interval_restored_from_cache = False - async def load(self) -> None: + async def load(self) -> bool: """Load and activate SED node features.""" if self._loaded: - return + return True + _LOGGER.debug("Load SED node %s from cache", self._node_info.mac) if await self._load_from_cache(): self._loaded = True @@ -126,6 +127,7 @@ async def load(self) -> None: await self._load_defaults() self._loaded = True self._features += SED_FEATURES + return self._loaded async def unload(self) -> None: """Deactivate and unload node features.""" @@ -146,10 +148,10 @@ async def unload(self) -> None: await super().unload() @raise_not_loaded - async def initialize(self) -> bool: + async def initialize(self) -> None: """Initialize SED node.""" if self._initialized: - return True + return self._awake_subscription = await self._message_subscribe( self._awake_response, @@ -157,7 +159,6 @@ async def initialize(self) -> bool: (NODE_AWAKE_RESPONSE_ID,), ) await super().initialize() - return True async def _load_defaults(self) -> None: """Load default configuration settings.""" diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 4b6615164..36fc06a6f 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -53,27 +53,23 @@ def __init__( self._sense_subscription: Callable[[], None] | None = None - async def load(self) -> bool: + async def load(self) -> None: """Load and activate Sense node features.""" if self._loaded: - return True + return _LOGGER.debug("Loading Sense node %s", self._node_info.mac) await super().load() self._setup_protocol(SENSE_FIRMWARE_SUPPORT, SENSE_FEATURES) - if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) - return True - - _LOGGER.warning("Load Sense node %s failed", self._node_info.mac) - return False + await self.initialize() + await self._loaded_callback(NodeEvent.LOADED, self.mac) @raise_not_loaded - async def initialize(self) -> bool: + async def initialize(self) -> None: """Initialize Sense node.""" if self._initialized: - return True + return self._sense_subscription = await self._message_subscribe( self._sense_report, @@ -81,7 +77,6 @@ async def initialize(self) -> bool: (SENSE_REPORT_ID,), ) await super().initialize() - return True async def unload(self) -> None: """Unload node.""" diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 62695d5ff..048ab72b3 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -46,27 +46,23 @@ def __init__( self._switch_subscription: Callable[[], None] | None = None self._switch = SwitchGroup() - async def load(self) -> bool: + async def load(self) -> None: """Load and activate Switch node features.""" if self._loaded: - return True + return _LOGGER.debug("Loading Switch node %s", self._node_info.mac) await super().load() self._setup_protocol(SWITCH_FIRMWARE_SUPPORT, SWITCH_FEATURES) - if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) - return True - - _LOGGER.warning("Load Switch node %s failed", self._node_info.mac) - return False + await self.initialize() + await self._loaded_callback(NodeEvent.LOADED, self.mac) @raise_not_loaded - async def initialize(self) -> bool: + async def initialize(self) -> None: """Initialize Switch node.""" if self._initialized: - return True + return self._switch_subscription = await self._message_subscribe( self._switch_response, @@ -74,7 +70,6 @@ async def initialize(self) -> bool: (NODE_SWITCH_GROUP_ID,), ) await super().initialize() - return True async def unload(self) -> None: """Unload node.""" diff --git a/tests/test_usb.py b/tests/test_usb.py index 2de50f097..5b574766e 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -575,7 +575,7 @@ async def test_stick_node_discovered_subscription( assert mac_awake_node == "5555555555555555" unsub_awake() - assert await stick.nodes["5555555555555555"].load() + assert await stick.nodes["5555555555555555"].load() is None assert stick.nodes["5555555555555555"].node_info.firmware == dt( 2011, 6, 27, 8, 55, 44, tzinfo=UTC ) @@ -1951,7 +1951,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sed.node_info.is_battery_powered assert test_sed.is_battery_powered - assert await test_sed.load() is None + assert await test_sed.load() assert sorted(test_sed.features) == sorted( ( pw_api.NodeFeature.AVAILABLE, @@ -2168,7 +2168,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign relay_state=None, ) await test_scan.update_node_details(node_info) - assert await test_scan.load() + assert await test_scan.load() is None # test motion reset timer assert test_scan.reset_timer == 10 @@ -2277,7 +2277,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) await test_scan.update_node_details(node_info) test_scan.cache_enabled = True - assert await test_scan.load() + await test_scan.load() assert sorted(test_scan.features) == sorted( ( pw_api.NodeFeature.AVAILABLE, @@ -2356,7 +2356,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign relay_state=None, ) await test_switch.update_node_details(node_info) - assert await test_switch.load() + await test_switch.load() # Switch specific defaults assert test_switch.switch is False @@ -2379,7 +2379,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign await test_switch.update_node_details(node_info) test_switch.cache_enabled = True assert test_switch.cache_enabled is True - assert await test_switch.load() + await test_switch.load() assert sorted(test_switch.features) == sorted( ( pw_api.NodeFeature.AVAILABLE, From efa24a2cfa7e58c49142bca65b48aa6964d2060c Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 19 Jul 2025 19:23:52 +0200 Subject: [PATCH 09/12] fix return value --- plugwise_usb/nodes/circle.py | 1 - tests/test_usb.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 79e2785fe..682d1193f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -924,7 +924,6 @@ async def load(self) -> None: self._setup_protocol(CIRCLE_FIRMWARE_SUPPORT, CIRCLE_FEATURES) await self._loaded_callback(NodeEvent.LOADED, self.mac) await self.initialize() - return True async def _load_from_cache(self) -> bool: """Load states from previous cached information. Returns True if successful.""" diff --git a/tests/test_usb.py b/tests/test_usb.py index 5b574766e..7044c4522 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -575,7 +575,7 @@ async def test_stick_node_discovered_subscription( assert mac_awake_node == "5555555555555555" unsub_awake() - assert await stick.nodes["5555555555555555"].load() is None + await stick.nodes["5555555555555555"].load() assert stick.nodes["5555555555555555"].node_info.firmware == dt( 2011, 6, 27, 8, 55, 44, tzinfo=UTC ) @@ -771,7 +771,7 @@ async def test_node_relay_and_power(self, monkeypatch: pytest.MonkeyPatch) -> No await stick.nodes["0098765432101234"].set_relay_lock(True) # Manually load node - assert await stick.nodes["0098765432101234"].load() + await stick.nodes["0098765432101234"].load() # Check relay_lock is set to False when not in cache assert stick.nodes["0098765432101234"].relay_lock @@ -849,7 +849,7 @@ async def test_node_relay_and_power(self, monkeypatch: pytest.MonkeyPatch) -> No with pytest.raises(pw_exceptions.NodeError): await stick.nodes["2222222222222222"].set_relay_init(True) - assert await stick.nodes["2222222222222222"].load() + await stick.nodes["2222222222222222"].load() self.test_init_relay_state_on = asyncio.Future() self.test_init_relay_state_off = asyncio.Future() unsub_inti_relay = stick.nodes["2222222222222222"].subscribe_to_feature_update( @@ -904,7 +904,7 @@ async def fake_get_missing_energy_logs(address: int) -> None: assert not stick.nodes["0098765432101234"].calibrated # Manually load node - assert await stick.nodes["0098765432101234"].load() + await stick.nodes["0098765432101234"].load() # Check calibration in loaded state assert stick.nodes["0098765432101234"].calibrated @@ -1951,7 +1951,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sed.node_info.is_battery_powered assert test_sed.is_battery_powered - assert await test_sed.load() + await test_sed.load() assert sorted(test_sed.features) == sorted( ( pw_api.NodeFeature.AVAILABLE, @@ -2168,7 +2168,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign relay_state=None, ) await test_scan.update_node_details(node_info) - assert await test_scan.load() is None + await test_scan.load() # test motion reset timer assert test_scan.reset_timer == 10 From 7885b2c474c493622bf36cb5c0dd3356ccdbf41b Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 22 Jul 2025 09:21:34 +0200 Subject: [PATCH 10/12] fix cache output to use name of enum --- plugwise_usb/nodes/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 775488363..f36fa061a 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -542,7 +542,7 @@ async def _scan_configure_update( daylight_mode=daylight_mode, ) self._set_cache(CACHE_MOTION_RESET_TIMER, str(motion_reset_timer)) - self._set_cache(CACHE_SCAN_SENSITIVITY, sensitivity_level) + self._set_cache(CACHE_SCAN_SENSITIVITY, sensitivity_level.name) if daylight_mode: self._set_cache(CACHE_SCAN_DAYLIGHT_MODE, "True") else: From 521e5cce1a3c223d371f32c907fa14060bca8331 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 22 Jul 2025 09:41:37 +0200 Subject: [PATCH 11/12] add await on _load_discovered_nodes --- plugwise_usb/network/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 660821085..bf5318ddf 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -488,7 +488,7 @@ async def _load_node(self, mac: str) -> bool: async def _load_stragglers(self) -> None: """Retry failed load operation.""" await sleep(NODE_RETRY_LOAD_INTERVAL) - while not self._load_discovered_nodes(): + while not await self._load_discovered_nodes(): await sleep(NODE_RETRY_LOAD_INTERVAL) async def _load_discovered_nodes(self) -> bool: From 04f44c3fc7da2a9a85be20c6fc1668ee987cb59e Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 22 Jul 2025 15:57:41 +0200 Subject: [PATCH 12/12] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ba3cf55..3a5e947b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## ONGOING + +- PR [295](https://github.com/plugwise/python-plugwise-usb/pull/295): Streamline of loading function, to allow nodes to load even if temporarily offline, especially for SED nodes. + ## v0.44.8 - PR [291](https://github.com/plugwise/python-plugwise-usb/pull/291): Collect send-queue depth via PriorityQueue.qsize(), this provides a more accurate result