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/plugwise_usb/api.py b/plugwise_usb/api.py index 33e7828fd..46ae63af0 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 in minutes at which the temperature and humidity are reported (1-60) 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..0681460af 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,16 +62,18 @@ 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 -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_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): @@ -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) -> int | None: + """Load 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) -> 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 + @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, @@ -686,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( @@ -697,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: @@ -746,22 +789,55 @@ 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 + + self._log_unexpected_response_ack(response.node_ack_type) + 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: + self._log_configure_failed("report interval") + return False + if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_ACCEPTED: + 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, + response.name, 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.""" @@ -787,6 +863,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/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 = [ diff --git a/tests/test_usb.py b/tests/test_usb.py index d0a139908..0d452a2c4 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 = None + def __init__(self) -> None: + """Initialize MockStickController.""" + self.send_response: list[pw_responses.PlugwiseResponse] = [] async def subscribe_to_messages( self, @@ -248,13 +250,23 @@ def dummy_method() -> None: return dummy_method + def append_response(self, response) -> None: + """Add response to queue.""" + self.send_response.append(response) + + def clear_responses(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.""" - return self.send_response + if self.send_response: + return self.send_response.pop(0) + return None aiofiles.threadpool.wrap.register(MagicMock)( @@ -1990,7 +2002,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 +2020,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 +2046,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 +2072,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 +2093,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 +2118,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 +2224,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 +2238,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 +2262,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 +2291,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 +2302,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.clear_responses() test_scan = pw_scan.PlugwiseScan( "1298347650AFBECD", pw_api.NodeType.SCAN, @@ -2353,6 +2371,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 +2406,34 @@ 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") + ) + + awake_response = pw_responses.NodeAwakeResponse() + awake_response.deserialize( + construct_message(b"004F555555555555555500", b"FFFE") + ) + + 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: + 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.""" @@ -2416,6 +2464,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): @@ -2436,34 +2485,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.send_response = sense_config_failed - 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) + responses = [ + sense_config_failed, + sense_config_accepted, + sense_config_report_interval_accepted, + ] + await run_awake_with_response(responses) 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( - 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 - 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_upper_bound == 65 @@ -2482,20 +2517,12 @@ 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( - 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 - 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 @@ -2518,37 +2545,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 - awake_response4 = pw_responses.NodeAwakeResponse() - awake_response4.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response4.timestamp = awake_response3.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 - 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 - awake_response5 = pw_responses.NodeAwakeResponse() - awake_response5.deserialize( - construct_message(b"004F555555555555555500", b"FFFE") - ) - awake_response5.timestamp = awake_response4.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 - 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 @@ -2567,23 +2578,45 @@ 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( - 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 - 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 + # 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.report_interval == 5 + + # Failed config + 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 + 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 + @pytest.mark.asyncio async def test_switch_node(self, monkeypatch: pytest.MonkeyPatch) -> None: """Testing properties of switch."""