From a3178a4e8842df25e4bf24b8135b315b226b63b3 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 14:26:31 +0200 Subject: [PATCH 01/11] Append report interval setting to Sense node configuration. Extend MockStickController with a response queue to send allow sending different responses sequentially --- plugwise_usb/api.py | 2 + plugwise_usb/messages/requests.py | 2 +- plugwise_usb/nodes/sense.py | 86 ++++++++++++++++++++- tests/test_usb.py | 121 ++++++++++++++---------------- 4 files changed, 145 insertions(+), 66 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 33e7828fd..774e9d82b 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -278,6 +278,7 @@ class SenseHysteresisConfig: temperature_upper_bound: float | None: upper temperature switching threshold (°C) temperature_lower_bound: float | None: lower temperature switching threshold (°C) temperature_direction: bool | None: True = switch ON when temperature rises; False = switch OFF when temperature rises + report_interval: int | None = None: Interval time at which the temperature and humidity are reported dirty: bool: Settings changed, device update pending Notes: @@ -293,6 +294,7 @@ class SenseHysteresisConfig: temperature_upper_bound: float | None = None temperature_lower_bound: float | None = None temperature_direction: bool | None = None + report_interval: int | None = None dirty: bool = False diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index a57a9860d..6068e3e62 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -1493,7 +1493,7 @@ def __init__( mac: bytes, interval: int, ): - """Initialize ScanLightCalibrateRequest message object.""" + """Initialize SenseReportIntervalRequest message object.""" super().__init__(send_fn, mac) self._args.append(Int(interval, length=2)) diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 5dd06e9ba..0675c0e13 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -18,7 +18,10 @@ ) from ..connection import StickController from ..exceptions import MessageError, NodeError -from ..messages.requests import SenseConfigureHysteresisRequest +from ..messages.requests import ( + SenseConfigureHysteresisRequest, + SenseReportIntervalRequest, +) from ..messages.responses import ( NODE_SWITCH_GROUP_ID, SENSE_REPORT_ID, @@ -59,6 +62,7 @@ CACHE_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND = "temperature_upper_bound" CACHE_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND = "temperature_lower_bound" CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION = "temperature_direction" +CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL = "report_interval" CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY = "sense_hysteresis_config_dirty" DEFAULT_SENSE_HYSTERESIS_HUMIDITY_ENABLED: Final = False @@ -69,6 +73,7 @@ DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final = 50.0 DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final = 50.0 DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final = True +DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL: Final = 15 class PlugwiseSense(NodeSED): @@ -175,6 +180,9 @@ async def _load_from_cache(self) -> bool: if (temperature_direction := self._temperature_direction_from_cache()) is None: dirty = True temperature_direction = DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION + if (report_interval := self._report_interval_from_cache()) is None: + dirty = True + report_interval = DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL dirty |= self._sense_hysteresis_config_dirty_from_cache() self._hysteresis_config = SenseHysteresisConfig( @@ -186,6 +194,7 @@ async def _load_from_cache(self) -> bool: temperature_upper_bound=temperature_upper_bound, temperature_lower_bound=temperature_lower_bound, temperature_direction=temperature_direction, + report_interval=report_interval, dirty=dirty, ) if dirty: @@ -248,6 +257,14 @@ def _temperature_direction_from_cache(self) -> bool | None: """Load Temperature hysteresis switch direction from cache.""" return self._get_cache_as_bool(CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION) + def _report_interval_from_cache(self) -> float | None: + """Report interval from cache.""" + if ( + report_interval := self._get_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL) + ) is not None: + return int(report_interval) + return None + def _sense_hysteresis_config_dirty_from_cache(self) -> bool: """Load sense hysteresis dirty from cache.""" if ( @@ -278,6 +295,7 @@ def hysteresis_config(self) -> SenseHysteresisConfig: temperature_upper_bound=self.temperature_upper_bound, temperature_lower_bound=self.temperature_lower_bound, temperature_direction=self.temperature_direction, + report_interval=self.report_interval, dirty=self.hysteresis_config_dirty, ) @@ -337,6 +355,13 @@ def temperature_direction(self) -> bool: return self._hysteresis_config.temperature_direction return DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION + @property + def report_interval(self) -> float: + """Temperature lower bound value.""" + if self._hysteresis_config.report_interval is not None: + return self._hysteresis_config.report_interval + return DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL + @property def hysteresis_config_dirty(self) -> bool: """Sense hysteresis configuration dirty flag.""" @@ -537,6 +562,31 @@ async def set_hysteresis_temperature_lower_bound(self, lower_bound: float) -> bo await self._sense_configure_update() return True + async def set_report_interval(self, report_interval: int) -> bool: + """Configure Sense measurement interval. + + Configuration request will be queued and will be applied the next time when node is awake for maintenance. + """ + _LOGGER.debug( + "set_report_interval | Device %s | %s -> %s", + self.name, + self._hysteresis_config.report_interval, + report_interval, + ) + if report_interval < 1 or report_interval > 60: + raise ValueError( + f"Invalid measurement interval {report_interval}. It must be between 1 and 60 minutes" + ) + if self._hysteresis_config.report_interval == report_interval: + return False + self._hysteresis_config = replace( + self._hysteresis_config, + report_interval=report_interval, + dirty=True, + ) + await self._sense_configure_update() + return True + async def set_hysteresis_temperature_direction(self, state: bool) -> bool: """Configure temperature hysteresis to switch on or off on increasing or decreasing direction. @@ -637,6 +687,7 @@ async def _run_awake_tasks(self) -> None: configure_result = await gather( self._configure_sense_humidity_task(), self._configure_sense_temperature_task(), + self._configure_sense_report_interval_task(), ) if all(configure_result): self._hysteresis_config = replace(self._hysteresis_config, dirty=False) @@ -645,10 +696,11 @@ async def _run_awake_tasks(self) -> None: else: _LOGGER.warning( "Sense hysteresis configuration partially failed for %s " - "(humidity=%s, temperature=%s); will retry on next wake.", + "(humidity=%s, temperature=%s, report_interval=%s); will retry on next wake.", self.name, configure_result[0], configure_result[1], + configure_result[2], ) await self.publish_feature_update_to_subscribers( NodeFeature.SENSE_HYSTERESIS, @@ -763,6 +815,35 @@ async def _configure_sense_temperature_task(self) -> bool: ) return False + async def _configure_sense_report_interval_task(self) -> bool: + """Configure Sense report interval setting. Returns True if successful.""" + if not self._hysteresis_config.dirty: + return True + request = SenseReportIntervalRequest( + self._send, + self._mac_in_bytes, + self.report_interval, + ) + if (response := await request.send()) is None: + _LOGGER.warning( + "No response from %s to configure report interval.", + self.name, + ) + return False + if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_FAILED: + _LOGGER.warning("Failed to configure report interval for %s", self.name) + return False + if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_ACCEPTED: + _LOGGER.debug("Successful configure report interval for %s", self.name) + return True + + _LOGGER.warning( + "Unexpected response ack type %s for %s", + response.node_ack_type, + self.name, + ) + return False + async def _sense_configure_update(self) -> None: """Push sense configuration update to cache.""" self._set_cache(CACHE_SENSE_HYSTERESIS_HUMIDITY_ENABLED, self.humidity_enabled) @@ -787,6 +868,7 @@ async def _sense_configure_update(self) -> None: self._set_cache( CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION, self.temperature_direction ) + self._set_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL, self.report_interval) self._set_cache( CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY, self.hysteresis_config_dirty ) diff --git a/tests/test_usb.py b/tests/test_usb.py index d0a139908..1701d184c 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -228,7 +228,7 @@ async def mkdir(self, path: str) -> None: class MockStickController: """Mock stick controller.""" - send_response = None + send_response: list = [] async def subscribe_to_messages( self, @@ -248,13 +248,19 @@ def dummy_method() -> None: return dummy_method + def append_response(self, send_response) -> None: + """Add response to queue.""" + self.send_response.append(send_response) + async def send( self, request: pw_requests.PlugwiseRequest, # type: ignore[name-defined] suppress_node_errors=True, ) -> pw_responses.PlugwiseResponse | None: # type: ignore[name-defined] """Submit request to queue and return response.""" - return self.send_response + if len(self.send_response) > 0: + return self.send_response.pop(0) + return None aiofiles.threadpool.wrap.register(MagicMock)( @@ -1990,7 +1996,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response1.deserialize( construct_message(b"004F555555555555555500", b"FFFE") ) - mock_stick_controller.send_response = sed_config_failed + mock_stick_controller.append_response(sed_config_failed) await test_sed._awake_response(awake_response1) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2008,7 +2014,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) assert await test_sed.set_awake_duration(20) assert test_sed.battery_config.dirty - mock_stick_controller.send_response = sed_config_accepted + mock_stick_controller.append_response(sed_config_accepted) await test_sed._awake_response(awake_response2) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2034,6 +2040,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response3.timestamp = awake_response2.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(sed_config_accepted) await test_sed._awake_response(awake_response3) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2059,6 +2066,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response4.timestamp = awake_response3.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(sed_config_accepted) await test_sed._awake_response(awake_response4) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2079,6 +2087,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response5.timestamp = awake_response4.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(sed_config_accepted) await test_sed._awake_response(awake_response5) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2103,6 +2112,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response6.timestamp = awake_response5.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(sed_config_accepted) await test_sed._awake_response(awake_response6) # pylint: disable=protected-access assert test_sed._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sed._delayed_task), timeout=2) @@ -2208,7 +2218,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response1.deserialize( construct_message(b"004F555555555555555500", b"FFFE") ) - mock_stick_controller.send_response = scan_config_failed + mock_stick_controller.append_response(scan_config_failed) await test_scan._awake_response(awake_response1) # pylint: disable=protected-access assert test_scan._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_scan._delayed_task), timeout=2) @@ -2222,7 +2232,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response2.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = scan_config_accepted + mock_stick_controller.append_response(scan_config_accepted) assert await test_scan.set_motion_reset_timer(25) assert test_scan.motion_config.dirty await test_scan._awake_response(awake_response2) # pylint: disable=protected-access @@ -2246,6 +2256,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response3.timestamp = awake_response2.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(scan_config_accepted) await test_scan._awake_response(awake_response3) # pylint: disable=protected-access assert test_scan._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_scan._delayed_task), timeout=2) @@ -2274,6 +2285,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response4.timestamp = awake_response3.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) + mock_stick_controller.append_response(scan_config_accepted) await test_scan._awake_response(awake_response4) # pylint: disable=protected-access assert test_scan._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_scan._delayed_task), timeout=2) @@ -2284,7 +2296,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) # scan with cache enabled - mock_stick_controller.send_response = None + mock_stick_controller.send_response = [] test_scan = pw_scan.PlugwiseScan( "1298347650AFBECD", pw_api.NodeType.SCAN, @@ -2353,6 +2365,8 @@ def fake_cache(dummy: object, setting: str) -> str | None: # noqa: PLR0911 PLR0 return "25.0" if setting == pw_sense.CACHE_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: return "25.0" + if setting == pw_sense.CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL: + return "15" return None def fake_cache_bool(dummy: object, setting: str) -> bool | None: @@ -2386,6 +2400,15 @@ def fake_cache_bool(dummy: object, setting: str) -> bool | None: construct_message(b"0100555555555555555500B6", b"0000") ) + sense_config_report_interval_accepted = pw_responses.NodeAckResponse() + sense_config_report_interval_accepted.deserialize( + construct_message(b"0100555555555555555500B3", b"0000") + ) + sense_config_report_interval_failed = pw_responses.NodeAckResponse() + sense_config_report_interval_failed.deserialize( + construct_message(b"0100555555555555555500B4", b"0000") + ) + async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] """Load callback for event.""" @@ -2440,28 +2463,20 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign awake_response1.deserialize( construct_message(b"004F555555555555555500", b"FFFE") ) - mock_stick_controller.send_response = sense_config_failed + mock_stick_controller.append_response(sense_config_failed) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - assert test_sense.hysteresis_config_dirty - # Successful config - awake_response2 = pw_responses.NodeAwakeResponse() - awake_response2.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response2.timestamp = awake_response1.timestamp + td( + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = sense_config_accepted - await test_sense._awake_response(awake_response2) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response2) # pylint: disable=protected-access + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) assert not test_sense.hysteresis_config_dirty @@ -2482,18 +2497,13 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sense.humidity_lower_bound == 55 # Successful config - awake_response3 = pw_responses.NodeAwakeResponse() - awake_response3.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response3.timestamp = awake_response2.timestamp + td( + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = sense_config_accepted - await test_sense._awake_response(awake_response3) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response3) # pylint: disable=protected-access + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) assert not test_sense.hysteresis_config_dirty @@ -2518,35 +2528,25 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_sense.set_hysteresis_temperature_direction(False) # Restore to original settings after failed config - awake_response4 = pw_responses.NodeAwakeResponse() - awake_response4.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response4.timestamp = awake_response3.timestamp + td( + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_failed) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = sense_config_failed - await test_sense._awake_response(awake_response4) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response4) # pylint: disable=protected-access + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) assert test_sense.hysteresis_config_dirty # Successful config - awake_response5 = pw_responses.NodeAwakeResponse() - awake_response5.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response5.timestamp = awake_response4.timestamp + td( + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = sense_config_accepted - await test_sense._awake_response(awake_response5) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response5) # pylint: disable=protected-access + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) assert not test_sense.hysteresis_config_dirty @@ -2567,18 +2567,13 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sense.temperature_lower_bound == 24 # Successful config - awake_response6 = pw_responses.NodeAwakeResponse() - awake_response6.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response6.timestamp = awake_response5.timestamp + td( + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( seconds=pw_sed.AWAKE_RETRY ) - mock_stick_controller.send_response = sense_config_accepted - await test_sense._awake_response(awake_response6) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - await test_sense._awake_response(awake_response6) # pylint: disable=protected-access + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access assert test_sense._delayed_task is not None # pylint: disable=protected-access await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) assert not test_sense.hysteresis_config_dirty From b0d05a4607610d28be73bc59eeb67f30f65b9dcf Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:15:53 +0200 Subject: [PATCH 02/11] remove repeated logmessages by pushing into function --- plugwise_usb/nodes/sense.py | 47 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 0675c0e13..a0b5137a7 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -738,10 +738,7 @@ async def _configure_sense_humidity_task(self) -> bool: self.humidity_direction, ) if (response := await request.send()) is None: - _LOGGER.warning( - "No response from %s to configure humidity hysteresis settings request", - self.name, - ) + self._log_configure_failed("humidity hysteresis") return False if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_FAILED: _LOGGER.warning( @@ -749,16 +746,10 @@ async def _configure_sense_humidity_task(self) -> bool: ) return False if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_ACCEPTED: - _LOGGER.debug( - "Successful configure humidity hysteresis settings for %s", self.name - ) + self._log_configure_success("humidity hysteresis") return True - _LOGGER.warning( - "Unexpected response ack type %s for %s", - response.node_ack_type, - self.name, - ) + self._log_unexpected_response_ack(response.node_ack_type) return False async def _configure_sense_temperature_task(self) -> bool: @@ -798,21 +789,13 @@ async def _configure_sense_temperature_task(self) -> bool: ) return False if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_FAILED: - _LOGGER.warning( - "Failed to configure temperature hysteresis settings for %s", self.name - ) + self._log_configure_failed("temperature hysteresis") return False if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_ACCEPTED: - _LOGGER.debug( - "Successful configure temperature hysteresis settings for %s", self.name - ) + self._log_configure_success("temperature hysteresis") return True - _LOGGER.warning( - "Unexpected response ack type %s for %s", - response.node_ack_type, - self.name, - ) + self._log_unexpected_response_ack(response.node_ack_type) return False async def _configure_sense_report_interval_task(self) -> bool: @@ -831,18 +814,30 @@ async def _configure_sense_report_interval_task(self) -> bool: ) return False if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_FAILED: - _LOGGER.warning("Failed to configure report interval for %s", self.name) + self._log_configure_failed("report interval") return False if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_ACCEPTED: - _LOGGER.debug("Successful configure report interval for %s", self.name) + self._log_configure_success("report interval") return True + self._log_unexpected_response_ack(response.node_ack_type) + return False + + def _log_unexpected_response_ack(self, response: NodeAckResponseType) -> None: + """Log unexpected response.""" _LOGGER.warning( "Unexpected response ack type %s for %s", response.node_ack_type, self.name, ) - return False + + def _log_configure_failed(self, parameter: str) -> None: + """Log configuration failed.""" + _LOGGER.warning("Failed to configure %s for %s", parameter, self.name) + + def _log_configure_success(self, parameter: str) -> None: + """Log configuration success.""" + _LOGGER.debug("Successful configure %s for %s", parameter, self.name) async def _sense_configure_update(self) -> None: """Push sense configuration update to cache.""" From 8977ed553ae56cec127d089de5738fd213a5f071 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:24:42 +0200 Subject: [PATCH 03/11] CR: fix docstring, type definition error --- plugwise_usb/api.py | 2 +- plugwise_usb/nodes/sense.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 774e9d82b..46ae63af0 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -278,7 +278,7 @@ class SenseHysteresisConfig: temperature_upper_bound: float | None: upper temperature switching threshold (°C) temperature_lower_bound: float | None: lower temperature switching threshold (°C) temperature_direction: bool | None: True = switch ON when temperature rises; False = switch OFF when temperature rises - report_interval: int | None = None: Interval time at which the temperature and humidity are reported + report_interval: int | None = None: Interval in minutes at which the temperature and humidity are reported (1-60) dirty: bool: Settings changed, device update pending Notes: diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index a0b5137a7..5334e8faa 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -66,14 +66,14 @@ CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY = "sense_hysteresis_config_dirty" DEFAULT_SENSE_HYSTERESIS_HUMIDITY_ENABLED: Final = False -DEFAULT_SENSE_HYSTERESIS_HUMIDITY_UPPER_BOUND: Final = 24.0 -DEFAULT_SENSE_HYSTERESIS_HUMIDITY_LOWER_BOUND: Final = 24.0 -DEFAULT_SENSE_HYSTERESIS_HUMIDITY_DIRECTION: Final = True -DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_ENABLED: Final = False -DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final = 50.0 -DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final = 50.0 -DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final = True -DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL: Final = 15 +DEFAULT_SENSE_HYSTERESIS_HUMIDITY_UPPER_BOUND: Final[float] = 24.0 +DEFAULT_SENSE_HYSTERESIS_HUMIDITY_LOWER_BOUND: Final[float] = 24.0 +DEFAULT_SENSE_HYSTERESIS_HUMIDITY_DIRECTION: Final[bool] = True +DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_ENABLED: Final[bool] = False +DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final[float] = 50.0 +DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final[float] = 50.0 +DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final[bool] = True +DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL: Final[int] = 15 class PlugwiseSense(NodeSED): @@ -257,8 +257,8 @@ def _temperature_direction_from_cache(self) -> bool | None: """Load Temperature hysteresis switch direction from cache.""" return self._get_cache_as_bool(CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION) - def _report_interval_from_cache(self) -> float | None: - """Report interval from cache.""" + def _report_interval_from_cache(self) -> int | None: + """Load report interval from cache.""" if ( report_interval := self._get_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL) ) is not None: From a8b869c9ca70722d47e26b0c8dc9eda6a13eccb2 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:33:23 +0200 Subject: [PATCH 04/11] CR: cleanup test_usb.py --- tests/test_usb.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 1701d184c..57d801686 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -252,13 +252,17 @@ def append_response(self, send_response) -> None: """Add response to queue.""" self.send_response.append(send_response) + def clear_response(self) -> None: + """Clear response queue.""" + self.send_response.clear() + async def send( self, request: pw_requests.PlugwiseRequest, # type: ignore[name-defined] suppress_node_errors=True, ) -> pw_responses.PlugwiseResponse | None: # type: ignore[name-defined] """Submit request to queue and return response.""" - if len(self.send_response) > 0: + if self.send_response: return self.send_response.pop(0) return None @@ -2296,7 +2300,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) # scan with cache enabled - mock_stick_controller.send_response = [] + mock_stick_controller.clear_response() test_scan = pw_scan.PlugwiseScan( "1298347650AFBECD", pw_api.NodeType.SCAN, From 2c0242b578ba563351019a9ae873b44c93b3ce0f Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:43:43 +0200 Subject: [PATCH 05/11] CR: type fix and docstring --- plugwise_usb/nodes/sense.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 5334e8faa..448e085c1 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -356,8 +356,8 @@ def temperature_direction(self) -> bool: return DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION @property - def report_interval(self) -> float: - """Temperature lower bound value.""" + def report_interval(self) -> int: + """Sense report interval in minutes.""" if self._hysteresis_config.report_interval is not None: return self._hysteresis_config.report_interval return DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL From 5e0eab4dc3d870a1423a5bed8449ec8097f0b4e3 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:46:38 +0200 Subject: [PATCH 06/11] CR: Make response queue instance-scoped (avoid cross-test bleed). --- tests/test_usb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 57d801686..4199835d9 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -228,7 +228,9 @@ async def mkdir(self, path: str) -> None: class MockStickController: """Mock stick controller.""" - send_response: list = [] + def __init__(self) -> None: + """Initialize MockStickController.""" + self.send_response: list[pw_responses.PlugwiseResponse] = [] async def subscribe_to_messages( self, From abdb291d1d1f471e403887d8db6f40618d4e2542 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Thu, 11 Sep 2025 16:48:19 +0200 Subject: [PATCH 07/11] CR: Fix AttributeError --- plugwise_usb/nodes/sense.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 448e085c1..0681460af 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -827,7 +827,7 @@ def _log_unexpected_response_ack(self, response: NodeAckResponseType) -> None: """Log unexpected response.""" _LOGGER.warning( "Unexpected response ack type %s for %s", - response.node_ack_type, + response.name, self.name, ) From 1f7fbff6ec3905c0d987f4583cbf8c0b88d1ddca Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Fri, 12 Sep 2025 09:03:45 +0200 Subject: [PATCH 08/11] update CHANGELOG, bump to 0.46.0 --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e820d1894..f78f298df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## ongoing +## 0.46.0 - 2025-09-12 +- PR [338](https://github.com/plugwise/python-plugwise-usb/pull/338): Append report interval to Sense node configuration - PR [333](https://github.com/plugwise/python-plugwise-usb/pull/333): Improve node_info_update and update_node_details logic ## 0.45.0 - 2025-09-03 diff --git a/pyproject.toml b/pyproject.toml index ec276e74c..4661415b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.45.0" +version = "0.46.0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From ecb23be69e04d91093ba8fec4339fe858977b13b Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Fri, 12 Sep 2025 09:22:08 +0200 Subject: [PATCH 09/11] include new feature in testing --- tests/test_usb.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_usb.py b/tests/test_usb.py index 4199835d9..fdc3e120a 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2445,6 +2445,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sense.temperature_upper_bound == 25 assert test_sense.temperature_lower_bound == 25 assert test_sense.temperature_direction + assert test_sense.report_interval == 15 # test humidity upper bound with pytest.raises(ValueError): @@ -2585,6 +2586,43 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert not test_sense.hysteresis_config_dirty assert test_sense.temperature_lower_bound == 24 + # test report interval + with pytest.raises(ValueError): + await test_sense.set_report_interval(0) + with pytest.raises(ValueError): + await test_sense.set_report_interval(61) + assert not await test_sense.set_report_interval(15) + assert not test_sense.hysteresis_config_dirty + assert await test_sense.set_report_interval(5) + assert test_sense.hysteresis_config_dirty + assert test_sense.hysteresis_config_dirty + assert test_sense.report_interval == 5 + + # Failed config + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_failed) + awake_response1.timestamp = awake_response1.timestamp + td( + seconds=pw_sed.AWAKE_RETRY + ) + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access + assert test_sense._delayed_task is not None # pylint: disable=protected-access + await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + assert test_sense.hysteresis_config_dirty + + # Successful config + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_accepted) + mock_stick_controller.append_response(sense_config_report_interval_accepted) + awake_response1.timestamp = awake_response1.timestamp + td( + seconds=pw_sed.AWAKE_RETRY + ) + await test_sense._awake_response(awake_response1) # pylint: disable=protected-access + assert test_sense._delayed_task is not None # pylint: disable=protected-access + await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + assert not test_sense.hysteresis_config_dirty + assert test_sense.report_interval == 5 + @pytest.mark.asyncio async def test_switch_node(self, monkeypatch: pytest.MonkeyPatch) -> None: """Testing properties of switch.""" From 1bce8c75e160c7a452db541435ad8f497cc04b4c Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Fri, 12 Sep 2025 10:02:25 +0200 Subject: [PATCH 10/11] CR: implement code deduplication and aming improvements --- tests/test_usb.py | 146 ++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 77 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index fdc3e120a..9cd12b05a 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -254,7 +254,7 @@ def append_response(self, send_response) -> None: """Add response to queue.""" self.send_response.append(send_response) - def clear_response(self) -> None: + def clear_responses(self) -> None: """Clear response queue.""" self.send_response.clear() @@ -2302,7 +2302,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign ) # scan with cache enabled - mock_stick_controller.clear_response() + mock_stick_controller.clear_responses() test_scan = pw_scan.PlugwiseScan( "1298347650AFBECD", pw_api.NodeType.SCAN, @@ -2415,6 +2415,23 @@ def fake_cache_bool(dummy: object, setting: str) -> bool | None: construct_message(b"0100555555555555555500B4", b"0000") ) + awake_response = pw_responses.NodeAwakeResponse() + awake_response.deserialize( + construct_message(b"004F555555555555555500", b"FFFE") + ) + + async def run_awake_with_response(responses): + """Wake node up and run tasks.""" + mock_stick_controller.clear_responses() + for r in responses: + mock_stick_controller.append_response(r) + awake_response.timestamp = awake_response.timestamp + td( + seconds=pw_sed.AWAKE_RETRY + ) + await test_sense._awake_response(awake_response) # pylint: disable=protected-access + assert test_sense._delayed_task is not None # pylint: disable=protected-access + await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ignore[name-defined] """Load callback for event.""" @@ -2466,26 +2483,20 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_sense.set_hysteresis_humidity_direction(False) # Restore to original settings after failed config - awake_response1 = pw_responses.NodeAwakeResponse() - awake_response1.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - mock_stick_controller.append_response(sense_config_failed) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) - - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_failed, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) + assert test_sense.hysteresis_config_dirty + + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert not test_sense.hysteresis_config_dirty assert test_sense.humidity_upper_bound == 65 @@ -2504,15 +2515,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sense.humidity_lower_bound == 55 # Successful config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert not test_sense.hysteresis_config_dirty assert test_sense.humidity_lower_bound == 55 @@ -2535,27 +2543,21 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert await test_sense.set_hysteresis_temperature_direction(False) # Restore to original settings after failed config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_failed) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_failed, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert test_sense.hysteresis_config_dirty # Successful config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert not test_sense.hysteresis_config_dirty assert test_sense.temperature_upper_bound == 26 @@ -2574,15 +2576,12 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_sense.temperature_lower_bound == 24 # Successful config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert not test_sense.hysteresis_config_dirty assert test_sense.temperature_lower_bound == 24 @@ -2595,31 +2594,24 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert not test_sense.hysteresis_config_dirty assert await test_sense.set_report_interval(5) assert test_sense.hysteresis_config_dirty - assert test_sense.hysteresis_config_dirty assert test_sense.report_interval == 5 # Failed config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_failed) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_failed, + ] + await run_awake_with_response(responses) assert test_sense.hysteresis_config_dirty # Successful config - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_accepted) - mock_stick_controller.append_response(sense_config_report_interval_accepted) - awake_response1.timestamp = awake_response1.timestamp + td( - seconds=pw_sed.AWAKE_RETRY - ) - await test_sense._awake_response(awake_response1) # pylint: disable=protected-access - assert test_sense._delayed_task is not None # pylint: disable=protected-access - await asyncio.wait_for(asyncio.shield(test_sense._delayed_task), timeout=2) + responses = [ + sense_config_accepted, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) assert not test_sense.hysteresis_config_dirty assert test_sense.report_interval == 5 From c2962e95bc92076e81e40031f479a612c28c1ea8 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Fri, 12 Sep 2025 10:21:36 +0200 Subject: [PATCH 11/11] CR: nitpick on naming/typedef --- tests/test_usb.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 9cd12b05a..0d452a2c4 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -250,9 +250,9 @@ def dummy_method() -> None: return dummy_method - def append_response(self, send_response) -> None: + def append_response(self, response) -> None: """Add response to queue.""" - self.send_response.append(send_response) + self.send_response.append(response) def clear_responses(self) -> None: """Clear response queue.""" @@ -2420,7 +2420,9 @@ def fake_cache_bool(dummy: object, setting: str) -> bool | None: construct_message(b"004F555555555555555500", b"FFFE") ) - async def run_awake_with_response(responses): + async def run_awake_with_response( + responses: list[pw_responses.PlugwiseResponse], + ): """Wake node up and run tasks.""" mock_stick_controller.clear_responses() for r in responses: