diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index beee04725..06879100b 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -156,9 +156,8 @@ async def _handle_stick_event(self, event: StickEvent) -> None: if not self._queue.is_running: self._queue.start(self._manager) await self.initialize_stick() - elif event == StickEvent.DISCONNECTED: - if self._queue.is_running: - await self._queue.stop() + elif event == StickEvent.DISCONNECTED and self._queue.is_running: + await self._queue.stop() async def initialize_stick(self) -> None: """Initialize connection to the USB-stick.""" diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index c2c138cb2..fcae17d5e 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -283,27 +283,34 @@ async def process_node_response(self, response: PlugwiseResponse) -> bool: async def _process_stick_response(self, stick_response: StickResponse) -> None: """Process incoming stick response.""" - if self._response_future.done(): + if ( + self._response_future.done() + or self._seq_id is None + or self._seq_id != stick_response.seq_id + ): return - if self._seq_id is None or self._seq_id != stick_response.seq_id: + + if stick_response.ack_id == StickResponseType.ACCEPT: return + if stick_response.ack_id == StickResponseType.TIMEOUT: self._response_timeout_expired(stick_timeout=True) - elif stick_response.ack_id == StickResponseType.FAILED: + return + + if stick_response.ack_id == StickResponseType.FAILED: self._unsubscribe_from_node() self._seq_id = None self._response_future.set_exception( NodeError(f"Stick failed request {self._seq_id}") ) - elif stick_response.ack_id == StickResponseType.ACCEPT: - pass - else: - _LOGGER.debug( - "Unknown StickResponseType %s at %s for request %s", - str(stick_response.ack_id), - stick_response, - self, - ) + return + + _LOGGER.debug( + "Unknown StickResponseType %s at %s for request %s", + str(stick_response.ack_id), + stick_response, + self, + ) async def _send_request(self, suppress_node_errors=False) -> PlugwiseResponse | None: """Send request.""" @@ -1287,11 +1294,11 @@ def __init__( self, send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]], mac: bytes, - taskId: int, + task_id: int, ) -> None: """Initialize NodeClearGroupMacRequest message object.""" super().__init__(send_fn, mac) - self._args.append(Int(taskId, length=2)) + self._args.append(Int(task_id, length=2)) class CircleSetScheduleValueRequest(PlugwiseRequest): diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 6ab9de3b3..16e6aa637 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -590,25 +590,17 @@ def __init__(self, protocol_version: str = "2.0") -> None: """Initialize NodeInfoResponse message object.""" super().__init__(b"0024") + self.datetime = DateTime() self._logaddress_pointer = LogAddr(0, length=8) - if protocol_version == "1.0": - # FIXME: Define "absoluteHour" variable - self.datetime = DateTime() + if protocol_version in ("1.0", "2.0"): + # FIXME 1.0: Define "absoluteHour" variable self._relay_state = Int(0, length=2) self._params += [ self.datetime, self._logaddress_pointer, self._relay_state, ] - elif protocol_version == "2.0": - self.datetime = DateTime() - self._relay_state = Int(0, length=2) - self._params += [ - self.datetime, - self._logaddress_pointer, - self._relay_state, - ] - elif protocol_version == "2.3": + if protocol_version == "2.3": # FIXME: Define "State_mask" variable self.state_mask = Int(0, length=2) self._params += [ diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 15de610e3..29553f0e0 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -208,30 +208,34 @@ async def _handle_stick_event(self, event: StickEvent) -> None: await gather(*[node.disconnect() for node in self._nodes.values()]) self._is_running = False - async def node_awake_message(self, response: PlugwiseResponse) -> bool: + async def node_awake_message(self, response: PlugwiseResponse) -> None: """Handle NodeAwakeResponse message.""" if not isinstance(response, NodeAwakeResponse): raise MessageError( f"Invalid response message type ({response.__class__.__name__}) received, expected NodeAwakeResponse" ) + mac = response.mac_decoded if self._awake_discovery.get(mac) is None: self._awake_discovery[mac] = response.timestamp - timedelta(seconds=15) + if mac in self._nodes: if self._awake_discovery[mac] < ( response.timestamp - timedelta(seconds=10) ): await self._notify_node_event_subscribers(NodeEvent.AWAKE, mac) self._awake_discovery[mac] = response.timestamp - return True + return + if (address := self._register.network_address(mac)) is None: if self._register.scan_completed: - return True + return + _LOGGER.debug( "Skip node awake message for %s because network registry address is unknown", mac, ) - return True + return if self._nodes.get(mac) is None: if ( @@ -243,7 +247,6 @@ async def node_awake_message(self, response: PlugwiseResponse) -> bool: ) else: _LOGGER.debug("duplicate maintenance awake discovery for %s", mac) - return True async def node_join_available_message(self, response: PlugwiseResponse) -> bool: """Handle NodeJoinAvailableResponse messages.""" @@ -275,11 +278,8 @@ async def node_rejoin_message(self, response: PlugwiseResponse) -> bool: raise NodeError(f"Failed to obtain address for node {mac}") if self._nodes.get(mac) is None: - if self._discover_sed_tasks.get(mac) is None: - self._discover_sed_tasks[mac] = create_task( - self._discover_battery_powered_node(address, mac) - ) - elif self._discover_sed_tasks[mac].done(): + task = self._discover_sed_tasks.get(mac) + if task is None or task.done(): self._discover_sed_tasks[mac] = create_task( self._discover_battery_powered_node(address, mac) ) diff --git a/plugwise_usb/network/registry.py b/plugwise_usb/network/registry.py index 23f285783..331d83581 100644 --- a/plugwise_usb/network/registry.py +++ b/plugwise_usb/network/registry.py @@ -145,7 +145,6 @@ async def retrieve_network_registration( return await self.retrieve_network_registration(address, retry=False) return None address = response.network_address - mac_of_node = response.registered_mac if (mac_of_node := response.registered_mac) == "FFFFFFFFFFFFFFFF": mac_of_node = "" return (address, mac_of_node) diff --git a/plugwise_usb/nodes/celsius.py b/plugwise_usb/nodes/celsius.py index 815c0f059..c8245a747 100644 --- a/plugwise_usb/nodes/celsius.py +++ b/plugwise_usb/nodes/celsius.py @@ -27,14 +27,13 @@ async def load(self) -> bool: """Load and activate node features.""" if self._loaded: return True - self._node_info.is_battery_powered = True + self._node_info.is_battery_powered = True + mac = self._node_info.mac if self._cache_enabled: - _LOGGER.debug( - "Load Celsius node %s from cache", self._node_info.mac - ) - if await self._load_from_cache(): - pass + _LOGGER.debug("Loading Celsius node %s from cache", mac) + if not await self._load_from_cache(): + _LOGGER.debug("Loading Celsius node %s from cache failed", mac) self._loaded = True self._setup_protocol( @@ -42,7 +41,8 @@ async def load(self) -> bool: (NodeFeature.INFO, NodeFeature.TEMPERATURE), ) if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) + await self._loaded_callback(NodeEvent.LOADED, mac) return True - _LOGGER.debug("Load of Celsius node %s failed", self._node_info.mac) + + _LOGGER.debug("Loading of Celsius node %s failed", mac) return False diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index dde3c5098..e9314637f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -38,7 +38,7 @@ EnergyCalibrationRequest, NodeInfoRequest, ) -from ..messages.responses import NodeInfoResponse, NodeResponse, NodeResponseType +from ..messages.responses import NodeInfoResponse, NodeResponseType from .helpers import EnergyCalibration, raise_not_loaded from .helpers.counter import EnergyCounters from .helpers.firmware import CIRCLE_FIRMWARE_SUPPORT @@ -532,7 +532,6 @@ async def energy_log_update(self, address: int | None) -> bool: async def _energy_log_records_load_from_cache(self) -> bool: """Load energy_log_record from cache.""" - cache_data = self._get_cache(CACHE_ENERGY_COLLECTION) if (cache_data := self._get_cache(CACHE_ENERGY_COLLECTION)) is None: _LOGGER.warning( "Failed to restore energy log records from cache for node %s", self.name @@ -733,7 +732,6 @@ async def clock_synchronize(self) -> bool: datetime.now(tz=UTC), self._node_protocols.max, ) - node_response: NodeResponse | None = await set_clock_request.send() if (node_response := await set_clock_request.send()) is None: _LOGGER.warning( "Failed to (re)set the internal clock of %s", @@ -849,12 +847,14 @@ async def initialize(self) -> bool: ) self._initialized = False return False + if not self._calibration and not await self.calibration_update(): _LOGGER.debug( "Failed to initialized node %s, no calibration", self._mac_in_str ) self._initialized = False return False + if ( self.skip_update(self._node_info, 30) and await self.node_info_update() is None @@ -869,7 +869,9 @@ async def initialize(self) -> bool: ) self._initialized = False return False - return await super().initialize() + + await super().initialize() + return True async def node_info_update( self, node_info: NodeInfoResponse | None = None @@ -1083,15 +1085,14 @@ def _correct_power_pulses(self, pulses: int, offset: int) -> float: async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any]: """Update latest state for given feature.""" states: dict[NodeFeature, Any] = {} - if not self._available: - if not await self.is_online(): - _LOGGER.debug( - "Node %s did not respond, unable to update state", self._mac_in_str - ) - for feature in features: - states[feature] = None - states[NodeFeature.AVAILABLE] = self.available_state - return states + if not self._available and not await self.is_online(): + _LOGGER.debug( + "Node %s did not respond, unable to update state", self._mac_in_str + ) + for feature in features: + states[feature] = None + states[NodeFeature.AVAILABLE] = self.available_state + return states for feature in features: if feature not in self._features: diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index 5026ec494..8f64a3efb 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -82,9 +82,8 @@ def add_pulse_log( # pylint: disable=too-many-arguments """Add pulse log.""" if self._pulse_collection.add_log( address, slot, timestamp, pulses, import_only - ): - if not import_only: - self.update() + ) and not import_only: + self.update() def get_pulse_logs(self) -> dict[int, dict[int, PulseLogRecord]]: """Return currently collected pulse logs.""" diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 41a4a6b59..7421a3f0d 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -935,8 +935,6 @@ def _missing_addresses_before( ): # Use consumption interval calc_interval_cons = timedelta(minutes=self._log_interval_consumption) - if self._log_interval_consumption == 0: - pass if not self._log_production: expected_timestamp = ( diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index 1f937b99c..95a53f231 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -337,14 +337,14 @@ def _setup_protocol( for feature in node_features: if ( required_version := FEATURE_SUPPORTED_AT_FIRMWARE.get(feature) - ) is not None: - if ( - self._node_protocols.min - <= required_version - <= self._node_protocols.max - and feature not in self._features - ): - self._features += (feature,) + ) is not None and ( + self._node_protocols.min + <= required_version + <= self._node_protocols.max + and feature not in self._features + ): + self._features += (feature,) + self._node_info.features = self._features async def reconnect(self) -> None: @@ -398,15 +398,15 @@ async def _load_from_cache(self) -> bool: return False return True - async def initialize(self) -> bool: + async def initialize(self) -> None: """Initialize node configuration.""" if self._initialized: - return True + return + self._initialization_delay_expired = datetime.now(tz=UTC) + timedelta( minutes=SUPPRESS_INITIALIZATION_WARNINGS ) self._initialized = True - return True async def _available_update_state( self, available: bool, timestamp: datetime | None = None diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 4137d66aa..cce83b064 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -110,12 +110,14 @@ async def initialize(self) -> bool: """Initialize Scan node.""" if self._initialized: return True + self._unsubscribe_switch_group = await self._message_subscribe( self._switch_group, self._mac_in_bytes, (NODE_SWITCH_GROUP_ID,), ) - return await super().initialize() + await super().initialize() + return True async def unload(self) -> None: """Unload node.""" @@ -164,14 +166,12 @@ def _daylight_mode_from_cache(self) -> bool: def _motion_from_cache(self) -> bool: """Load motion state from cache.""" if (cached_motion_state := self._get_cache(CACHE_MOTION_STATE)) is not None: - if cached_motion_state == "True": - if ( - motion_timestamp := self._motion_timestamp_from_cache() - ) is not None: - if ( - datetime.now(tz=UTC) - motion_timestamp - ).seconds < self._reset_timer_from_cache() * 60: - return True + if ( + cached_motion_state == "True" + and (motion_timestamp := self._motion_timestamp_from_cache()) is not None + and (datetime.now(tz=UTC) - motion_timestamp).seconds < self._reset_timer_from_cache() * 60 + ): + return True return False return SCAN_DEFAULT_MOTION_STATE diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 69197c19b..b0c15fb51 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -144,12 +144,14 @@ async def initialize(self) -> bool: """Initialize SED node.""" if self._initialized: return True + self._awake_subscription = await self._message_subscribe( self._awake_response, self._mac_in_bytes, (NODE_AWAKE_RESPONSE_ID,), ) - return await super().initialize() + await super().initialize() + return True def _load_defaults(self) -> None: """Load default configuration settings.""" @@ -498,6 +500,7 @@ async def _awake_response(self, response: PlugwiseResponse) -> bool: raise MessageError( f"Invalid response message type ({response.__class__.__name__}) received, expected NodeAwakeResponse" ) + _LOGGER.debug("Device %s is awake for %s", self.name, response.awake_type) self._set_cache(CACHE_AWAKE_TIMESTAMP, response.timestamp) await self._available_update_state(True, response.timestamp) @@ -505,7 +508,6 @@ async def _awake_response(self, response: PlugwiseResponse) -> bool: # Pre populate the last awake timestamp if self._last_awake.get(response.awake_type) is None: self._last_awake[response.awake_type] = response.timestamp - # Skip awake messages when they are shortly after each other elif ( self._last_awake[response.awake_type] + timedelta(seconds=AWAKE_RETRY) @@ -684,7 +686,6 @@ async def sed_configure( # pylint: disable=too-many-arguments maintenance_interval, sleep_duration, ) - response = await request.send() if (response := await request.send()) is None: self._new_battery_config = BatteryConfig() _LOGGER.warning( diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index e73724ef0..1e4f0ac20 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -72,12 +72,14 @@ async def initialize(self) -> bool: """Initialize Sense node.""" if self._initialized: return True + self._sense_subscription = await self._message_subscribe( self._sense_report, self._mac_in_bytes, (SENSE_REPORT_ID,), ) - return await super().initialize() + 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 bf84fb10a..80b1fc191 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -67,12 +67,14 @@ async def initialize(self) -> bool: """Initialize Switch node.""" if self._initialized: return True + self._switch_subscription = await self._message_subscribe( self._switch_group, self._mac_in_bytes, (NODE_SWITCH_GROUP_ID,), ) - return await super().initialize() + 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 7d299035d..7a8542dc4 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2630,6 +2630,12 @@ async def test_node_discovery_and_load( pw_api.NodeFeature.SWITCH, ) ) + assert state[pw_api.NodeFeature.AVAILABLE].state + assert state[pw_api.NodeFeature.BATTERY].maintenance_interval == 60 + assert state[pw_api.NodeFeature.BATTERY].awake_duration == 10 + assert not state[pw_api.NodeFeature.BATTERY].clock_sync + assert state[pw_api.NodeFeature.BATTERY].clock_interval == 25200 + assert state[pw_api.NodeFeature.BATTERY].sleep_duration == 60 # endregion # test disable cache