From cc6bcf642c461f5023506f33af08a0877834ff71 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 14:12:40 +0200 Subject: [PATCH 01/22] store MotionSensitivity enum which makes communication to HA easier --- plugwise_usb/api.py | 2 +- plugwise_usb/nodes/scan.py | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 0350fdadb..f8cbd36f9 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -233,7 +233,7 @@ class MotionConfig: Attributes: reset_timer: int | None: Motion reset timer in minutes before the motion detection is switched off. daylight_mode: bool | None: Motion detection when light level is below threshold. - sensitivity_level: int | None: Motion sensitivity level. + sensitivity_level: MotionSensitivity | None: Motion sensitivity level. dirty: bool: Settings changed, device update pending """ diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index fc3e84c46..53188a6c9 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -198,7 +198,7 @@ def _reset_timer_from_cache(self) -> int | None: return int(reset_timer) return None - def _sensitivity_level_from_cache(self) -> int | None: + def _sensitivity_level_from_cache(self) -> MotionSensitivity | None: """Load sensitivity level from cache.""" if ( sensitivity_level := self._get_cache( @@ -326,14 +326,27 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: await self._scan_configure_update() return True - async def set_motion_sensitivity_level(self, level: int) -> bool: + async def set_motion_sensitivity_level(self, level: MotionSensitivity | int | str) -> bool: """Configure the motion sensitivity level.""" _LOGGER.debug( "set_motion_sensitivity_level | Device %s | %s -> %s", self.name, self._motion_config.sensitivity_level, - level, + str(level), ) + if isinstance(level,int): + try: + level = MotionSensitivity(level) + except ValueError: + _LOGGER.exception("MotionSensitivity for %s: value error ", self._mac_in_str) + return False + + if isinstance(level,str): + try: + level = MotionSensitivity[level] + except KeyError: + _LOGGER.exception("MotionSensitivity for %s: unknown level %s", self._mac_in_str,level) + return False if self._motion_config.sensitivity_level == level: return False self._motion_config = replace( @@ -447,7 +460,7 @@ async def scan_configure(self) -> bool: self._send, self._mac_in_bytes, self._motion_config.reset_timer, - self._motion_config.sensitivity_level, + self._motion_config.sensitivity_level.value, self._motion_config.daylight_mode, ) if (response := await request.send()) is None: @@ -478,7 +491,7 @@ async def _scan_configure_update(self) -> None: ) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, - str(MotionSensitivity(self._motion_config.sensitivity_level).name), + self._motion_config.sensitivity_level.name, ) self._set_cache( CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self._motion_config.daylight_mode) From 6bd701328502475008b13ecb4477445f209f8c68 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 18 Aug 2025 12:14:53 +0000 Subject: [PATCH 02/22] fixup: mdi_scan Python code reformatted using Ruff --- plugwise_usb/nodes/scan.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 53188a6c9..aa9eb9a68 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -326,7 +326,9 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: await self._scan_configure_update() return True - async def set_motion_sensitivity_level(self, level: MotionSensitivity | int | str) -> bool: + async def set_motion_sensitivity_level( + self, level: MotionSensitivity | int | str + ) -> bool: """Configure the motion sensitivity level.""" _LOGGER.debug( "set_motion_sensitivity_level | Device %s | %s -> %s", @@ -334,18 +336,24 @@ async def set_motion_sensitivity_level(self, level: MotionSensitivity | int | st self._motion_config.sensitivity_level, str(level), ) - if isinstance(level,int): + if isinstance(level, int): try: level = MotionSensitivity(level) except ValueError: - _LOGGER.exception("MotionSensitivity for %s: value error ", self._mac_in_str) + _LOGGER.exception( + "MotionSensitivity for %s: value error ", self._mac_in_str + ) return False - - if isinstance(level,str): + + if isinstance(level, str): try: level = MotionSensitivity[level] except KeyError: - _LOGGER.exception("MotionSensitivity for %s: unknown level %s", self._mac_in_str,level) + _LOGGER.exception( + "MotionSensitivity for %s: unknown level %s", + self._mac_in_str, + level, + ) return False if self._motion_config.sensitivity_level == level: return False From 9a147d500963fcbae0b57e6c8563ac194fa86bc9 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 14:37:55 +0200 Subject: [PATCH 03/22] CR: Catched missing update --- plugwise_usb/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index f8cbd36f9..191fcdbba 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -240,7 +240,7 @@ class MotionConfig: daylight_mode: bool | None = None reset_timer: int | None = None - sensitivity_level: int | None = None + sensitivity_level: MotionSensitivity | None = None dirty: bool = False @@ -659,7 +659,10 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: """ - async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: + async def set_motion_sensitivity_level( + self, + level: MotionSensitivity | int | str + ) -> bool: """Configure motion sensitivity level. Description: From 247d762ab5174fab521470b5d90737de5a9f86bc Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 16:28:20 +0200 Subject: [PATCH 04/22] reference to self-functions which return default values of config-object contains None --- plugwise_usb/nodes/scan.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index aa9eb9a68..ae5b75268 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -467,9 +467,9 @@ async def scan_configure(self) -> bool: request = ScanConfigureRequest( self._send, self._mac_in_bytes, - self._motion_config.reset_timer, - self._motion_config.sensitivity_level.value, - self._motion_config.daylight_mode, + self.reset_timer, + self.sensitivity_level.value, + self.daylight_mode, ) if (response := await request.send()) is None: _LOGGER.warning( @@ -495,16 +495,16 @@ async def scan_configure(self) -> bool: async def _scan_configure_update(self) -> None: """Push scan configuration update to cache.""" self._set_cache( - CACHE_SCAN_CONFIG_RESET_TIMER, str(self._motion_config.reset_timer) + CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer) ) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, self._motion_config.sensitivity_level.name, ) self._set_cache( - CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self._motion_config.daylight_mode) + CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode) ) - self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self._motion_config.dirty)) + self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty)) await gather( self.publish_feature_update_to_subscribers( NodeFeature.MOTION_CONFIG, From 722734a3423b8db27fa6fe43fda7739d86769c6a Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 18 Aug 2025 14:29:41 +0000 Subject: [PATCH 05/22] fixup: mdi_scan Python code reformatted using Ruff --- plugwise_usb/api.py | 3 +-- plugwise_usb/nodes/scan.py | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 191fcdbba..38d839266 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -660,8 +660,7 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: """ async def set_motion_sensitivity_level( - self, - level: MotionSensitivity | int | str + self, level: MotionSensitivity | int | str ) -> bool: """Configure motion sensitivity level. diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index ae5b75268..548694a6b 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -494,16 +494,12 @@ async def scan_configure(self) -> bool: async def _scan_configure_update(self) -> None: """Push scan configuration update to cache.""" - self._set_cache( - CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer) - ) + self._set_cache(CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer)) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, self._motion_config.sensitivity_level.name, ) - self._set_cache( - CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode) - ) + self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode)) self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty)) await gather( self.publish_feature_update_to_subscribers( From 96307d0203719e8b50ced69126374a1730277668 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 16:34:24 +0200 Subject: [PATCH 06/22] print wrong value of level in de log --- plugwise_usb/nodes/scan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 548694a6b..6c5b7140f 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -341,7 +341,9 @@ async def set_motion_sensitivity_level( level = MotionSensitivity(level) except ValueError: _LOGGER.exception( - "MotionSensitivity for %s: value error ", self._mac_in_str + "MotionSensitivity for %s: invalid numeric value %s", + self._mac_in_str, + str(level), ) return False From 9b13066546ae23fb1ba04ba2a7215dee03e1ba61 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 18 Aug 2025 14:35:17 +0000 Subject: [PATCH 07/22] fixup: mdi_scan Python code reformatted using Ruff --- plugwise_usb/nodes/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 6c5b7140f..8c17551ce 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -341,7 +341,7 @@ async def set_motion_sensitivity_level( level = MotionSensitivity(level) except ValueError: _LOGGER.exception( - "MotionSensitivity for %s: invalid numeric value %s", + "MotionSensitivity for %s: invalid numeric value %s", self._mac_in_str, str(level), ) From 22b21d31f9606b2edea238545a627c586fbcef1f Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 17:18:46 +0200 Subject: [PATCH 08/22] CR: update node.py as well --- plugwise_usb/nodes/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index 5c223ffa0..fd3622a65 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -787,7 +787,7 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: raise NotImplementedError() @raise_not_loaded - async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: + async def set_motion_sensitivity_level(self, level: MotionSensitivity |int | str) -> bool: """Configure motion sensitivity level.""" if NodeFeature.MOTION not in self._features: raise FeatureError( From b5a44c2b733fd5199bae21df683888534ee321c3 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Mon, 18 Aug 2025 17:21:35 +0200 Subject: [PATCH 09/22] release 0.44.12a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6b79f94dd..1e6f29c52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.12a1" +version = "0.44.12a2" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ From 96e4df521f005f01cca36976b6273a2c14ce5f75 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 19 Aug 2025 08:18:45 +0200 Subject: [PATCH 10/22] CR: Nitpick on formatting --- plugwise_usb/nodes/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index fd3622a65..c861da841 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -787,7 +787,9 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: raise NotImplementedError() @raise_not_loaded - async def set_motion_sensitivity_level(self, level: MotionSensitivity |int | str) -> bool: + async def set_motion_sensitivity_level( + self, level: MotionSensitivity | int | str + ) -> bool: """Configure motion sensitivity level.""" if NodeFeature.MOTION not in self._features: raise FeatureError( From f53663108a6df7c79ef151d8b5378c43e7b952b3 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Tue, 19 Aug 2025 17:35:26 +0200 Subject: [PATCH 11/22] expose scheduling of light calibration --- plugwise_usb/api.py | 7 +++++++ plugwise_usb/nodes/scan.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 38d839266..80ce33534 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -680,6 +680,13 @@ async def set_motion_sensitivity_level( """ + async def scan_calibrate_light(self) -> bool: + """Request to calibration light sensitivity of Scan device. + + Description: + Request to calibration light sensitivity of Scan device. + """ + async def set_relay_init(self, state: bool) -> bool: """Change the initial state of the relay. diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 8c17551ce..20123f5ef 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -86,7 +86,7 @@ def __init__( self._motion_state = MotionState() self._motion_config = MotionConfig() - + self._scan_calibrate_light_scheduled: bool = False self._configure_daylight_mode_task: Task[Coroutine[Any, Any, None]] | None = ( None ) @@ -449,6 +449,8 @@ async def _run_awake_tasks(self) -> None: await super()._run_awake_tasks() if self._motion_config.dirty: await self._configure_scan_task() + if self._scan_calibrate_light_scheduled: + await self._scan_calibrate_light() await self.publish_feature_update_to_subscribers( NodeFeature.MOTION_CONFIG, self._motion_config, @@ -511,7 +513,11 @@ async def _scan_configure_update(self) -> None: self.save_cache(), ) - async def scan_calibrate_light(self) -> bool: + async def scan_calibrate_light(self) -> None: + """Schedule light sensitivity calibration of Scan device.""" + self._scan_calibrate_light_scheduled = True + + async def _scan_calibrate_light(self) -> bool: """Request to calibration light sensitivity of Scan device.""" request = ScanLightCalibrateRequest(self._send, self._mac_in_bytes) if (response := await request.send()) is not None: @@ -519,6 +525,7 @@ async def scan_calibrate_light(self) -> bool: response.node_ack_type == NodeAckResponseType.SCAN_LIGHT_CALIBRATION_ACCEPTED ): + self._scan_calibrate_light_scheduled = False return True return False raise NodeTimeout( From 32f66ff2e197416ba82f026f45b9f3251140acff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 Aug 2025 19:15:34 +0200 Subject: [PATCH 12/22] Revert api/set_motion_sensitivity_level() typing --- plugwise_usb/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 80ce33534..97873d328 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -659,9 +659,7 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: """ - async def set_motion_sensitivity_level( - self, level: MotionSensitivity | int | str - ) -> bool: + async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: """Configure motion sensitivity level. Description: From b79775522f81f1aa27309b0fb0532181e5ca85db Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 Aug 2025 19:17:50 +0200 Subject: [PATCH 13/22] Revert node/set_motion_sensitivity_level() typing --- plugwise_usb/nodes/node.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index c861da841..5c223ffa0 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -787,9 +787,7 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: raise NotImplementedError() @raise_not_loaded - async def set_motion_sensitivity_level( - self, level: MotionSensitivity | int | str - ) -> bool: + async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: """Configure motion sensitivity level.""" if NodeFeature.MOTION not in self._features: raise FeatureError( From 3516508036597269171b9b991915fc51c45f9307 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 Aug 2025 19:43:02 +0200 Subject: [PATCH 14/22] Assure proper input-output --- plugwise_usb/nodes/scan.py | 39 ++++++++------------------------------ 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 20123f5ef..bc24eb7f7 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -86,7 +86,7 @@ def __init__( self._motion_state = MotionState() self._motion_config = MotionConfig() - self._scan_calibrate_light_scheduled: bool = False + self._scan_calibrate_light_scheduled = False self._configure_daylight_mode_task: Task[Coroutine[Any, Any, None]] | None = ( None ) @@ -165,7 +165,7 @@ async def _load_from_cache(self) -> bool: self._motion_config = MotionConfig( daylight_mode=daylight_mode, reset_timer=reset_timer, - sensitivity_level=sensitivity_level, + sensitivity_level=sensitivity_level.value, dirty=dirty, ) if dirty: @@ -326,42 +326,19 @@ async def set_motion_reset_timer(self, minutes: int) -> bool: await self._scan_configure_update() return True - async def set_motion_sensitivity_level( - self, level: MotionSensitivity | int | str - ) -> bool: + async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: """Configure the motion sensitivity level.""" _LOGGER.debug( "set_motion_sensitivity_level | Device %s | %s -> %s", self.name, self._motion_config.sensitivity_level, - str(level), + level.value, ) - if isinstance(level, int): - try: - level = MotionSensitivity(level) - except ValueError: - _LOGGER.exception( - "MotionSensitivity for %s: invalid numeric value %s", - self._mac_in_str, - str(level), - ) - return False - - if isinstance(level, str): - try: - level = MotionSensitivity[level] - except KeyError: - _LOGGER.exception( - "MotionSensitivity for %s: unknown level %s", - self._mac_in_str, - level, - ) - return False - if self._motion_config.sensitivity_level == level: + if self._motion_config.sensitivity_level == level.value: return False self._motion_config = replace( self._motion_config, - sensitivity_level=level, + sensitivity_level=level.value, dirty=True, ) await self._scan_configure_update() @@ -472,7 +449,7 @@ async def scan_configure(self) -> bool: self._send, self._mac_in_bytes, self.reset_timer, - self.sensitivity_level.value, + self.sensitivity_level, self.daylight_mode, ) if (response := await request.send()) is None: @@ -501,7 +478,7 @@ async def _scan_configure_update(self) -> None: self._set_cache(CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer)) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, - self._motion_config.sensitivity_level.name, + self._motion_config.sensitivity_level, ) self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode)) self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty)) From 68ab73339d975bd1dfdfa26cace5491def66e361 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 Aug 2025 19:49:07 +0200 Subject: [PATCH 15/22] Fix set_motion_sensitivity_level tests --- tests/test_usb.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 2c0917bc6..f76883cb3 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1834,7 +1834,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign await test_node.set_motion_daylight_mode(True) with pytest.raises(pw_exceptions.NodeError): - await test_node.set_motion_sensitivity_level(20) + await test_node.set_motion_sensitivity_level(pw_api.MotionSensitivity.HIGH) with pytest.raises(pw_exceptions.NodeError): await test_node.set_motion_reset_timer(5) @@ -1865,7 +1865,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign await test_node.set_motion_daylight_mode(True) with pytest.raises(pw_exceptions.FeatureError): - await test_node.set_motion_sensitivity_level(20) + await test_node.set_motion_sensitivity_level(pw_api.MotionSensitivity.HIGH) with pytest.raises(pw_exceptions.FeatureError): await test_node.set_motion_reset_timer(5) @@ -1892,7 +1892,7 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign with pytest.raises(NotImplementedError): await test_node.set_motion_daylight_mode(True) with pytest.raises(NotImplementedError): - await test_node.set_motion_sensitivity_level(20) + await test_node.set_motion_sensitivity_level(pw_api.MotionSensitivity.HIGH) with pytest.raises(NotImplementedError): await test_node.set_motion_reset_timer(5) @@ -2242,10 +2242,14 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign # test motion sensitivity level assert test_scan.sensitivity_level == 30 assert test_scan.motion_config.sensitivity_level == 30 - assert not await test_scan.set_motion_sensitivity_level(30) + assert not await test_scan.set_motion_sensitivity_level( + pw_api.MotionSensitivity.MEDIUM + ) assert not test_scan.motion_config.dirty - assert await test_scan.set_motion_sensitivity_level(20) + assert await test_scan.set_motion_sensitivity_level( + pw_api.MotionSensitivity.HIGH + ) assert test_scan.motion_config.dirty awake_response4 = pw_responses.NodeAwakeResponse() awake_response4.deserialize( From 7e010a556803968dbfae3e3161d9c4f7b14f6f13 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 Aug 2025 19:57:53 +0200 Subject: [PATCH 16/22] Fix sensitivity_level property --- plugwise_usb/nodes/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index bc24eb7f7..74ebb7fd4 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -278,7 +278,7 @@ def sensitivity_level(self) -> int: """Sensitivity level of motion sensor.""" if self._motion_config.sensitivity_level is not None: return self._motion_config.sensitivity_level - return DEFAULT_SENSITIVITY + return DEFAULT_SENSITIVITY.value # endregion # region Configuration actions From 1b6411906f43a4a462b5c5e352b294dc4f17cf8b Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 11:14:49 +0200 Subject: [PATCH 17/22] convert to value just before call to ScanConfigureRequest --- plugwise_usb/nodes/scan.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 74ebb7fd4..c986aa645 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -165,7 +165,7 @@ async def _load_from_cache(self) -> bool: self._motion_config = MotionConfig( daylight_mode=daylight_mode, reset_timer=reset_timer, - sensitivity_level=sensitivity_level.value, + sensitivity_level=sensitivity_level, dirty=dirty, ) if dirty: @@ -274,11 +274,11 @@ def reset_timer(self) -> int: return DEFAULT_RESET_TIMER @property - def sensitivity_level(self) -> int: + def sensitivity_level(self) -> MotionSensitivity: """Sensitivity level of motion sensor.""" if self._motion_config.sensitivity_level is not None: return self._motion_config.sensitivity_level - return DEFAULT_SENSITIVITY.value + return DEFAULT_SENSITIVITY # endregion # region Configuration actions @@ -331,14 +331,14 @@ async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: _LOGGER.debug( "set_motion_sensitivity_level | Device %s | %s -> %s", self.name, - self._motion_config.sensitivity_level, - level.value, + self._motion_config.sensitivity_level.name, + level.name, ) - if self._motion_config.sensitivity_level == level.value: + if self._motion_config.sensitivity_level == level: return False self._motion_config = replace( self._motion_config, - sensitivity_level=level.value, + sensitivity_level=level, dirty=True, ) await self._scan_configure_update() @@ -449,7 +449,7 @@ async def scan_configure(self) -> bool: self._send, self._mac_in_bytes, self.reset_timer, - self.sensitivity_level, + self.sensitivity_level.value, self.daylight_mode, ) if (response := await request.send()) is None: @@ -478,7 +478,7 @@ async def _scan_configure_update(self) -> None: self._set_cache(CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer)) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, - self._motion_config.sensitivity_level, + self._motion_config.sensitivity_level.name, ) self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode)) self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty)) From 1d56d5f5aba8c79d189ff39fea580837bbdbfec0 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 11:20:40 +0200 Subject: [PATCH 18/22] cherry-pick updates to test_usb.py --- tests/test_usb.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index f76883cb3..7b20edc5e 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1494,7 +1494,7 @@ async def test_creating_request_messages(self) -> None: self.dummy_fn, b"1111222233334444", 5, # Delay in minutes when signal is send when no motion is detected - 30, # Sensitivity of Motion sensor (High, Medium, Off) + pw_api.MotionSensitivity.MEDIUM, # Sensitivity of Motion sensor (High, Medium, Off) False, # Daylight override to only report motion when lightlevel is below calibrated level ) assert ( @@ -2240,8 +2240,10 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign assert test_scan.motion_config.daylight_mode # test motion sensitivity level - assert test_scan.sensitivity_level == 30 - assert test_scan.motion_config.sensitivity_level == 30 + assert test_scan.sensitivity_level == pw_api.MotionSensitivity.MEDIUM + assert ( + test_scan.motion_config.sensitivity_level == pw_api.MotionSensitivity.MEDIUM + ) assert not await test_scan.set_motion_sensitivity_level( pw_api.MotionSensitivity.MEDIUM ) @@ -2261,8 +2263,10 @@ async def load_callback(event: pw_api.NodeEvent, mac: str) -> None: # type: ign await test_scan._awake_response(awake_response4) # pylint: disable=protected-access await asyncio.sleep(0.001) # Ensure time for task to be executed assert not test_scan.motion_config.dirty - assert test_scan.sensitivity_level == 20 - assert test_scan.motion_config.sensitivity_level == 20 + assert test_scan.sensitivity_level == pw_api.MotionSensitivity.HIGH + assert ( + test_scan.motion_config.sensitivity_level == pw_api.MotionSensitivity.HIGH + ) # scan with cache enabled mock_stick_controller.send_response = None From 169603106f5621204245aaa02b9f0134cae8fd1d Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 12:53:13 +0200 Subject: [PATCH 19/22] return type change to indicate already or newly activated scan_light_calibration --- plugwise_usb/nodes/scan.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index c986aa645..a52b0ac3e 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -490,9 +490,16 @@ async def _scan_configure_update(self) -> None: self.save_cache(), ) - async def scan_calibrate_light(self) -> None: - """Schedule light sensitivity calibration of Scan device.""" + async def scan_calibrate_light(self) -> bool: + """Schedule light sensitivity calibration of Scan device. + + Returns True when scheduling was newly activated; + False if it was already scheduled. + """ + if self._scan_calibrate_light_scheduled: + return False self._scan_calibrate_light_scheduled = True + return True async def _scan_calibrate_light(self) -> bool: """Request to calibration light sensitivity of Scan device.""" From 01d1db41baab30ea6fbfd77feee34d8ed37e5cf9 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 13:05:03 +0200 Subject: [PATCH 20/22] CR: remove unwanted exception --- plugwise_usb/nodes/scan.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index a52b0ac3e..71351acd1 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -19,7 +19,7 @@ ) from ..connection import StickController from ..constants import MAX_UINT_2 -from ..exceptions import MessageError, NodeError, NodeTimeout +from ..exceptions import MessageError, NodeError from ..messages.requests import ScanConfigureRequest, ScanLightCalibrateRequest from ..messages.responses import ( NODE_SWITCH_GROUP_ID, @@ -331,7 +331,7 @@ async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> bool: _LOGGER.debug( "set_motion_sensitivity_level | Device %s | %s -> %s", self.name, - self._motion_config.sensitivity_level.name, + self.sensitivity_level.name, level.name, ) if self._motion_config.sensitivity_level == level: @@ -504,17 +504,23 @@ async def scan_calibrate_light(self) -> bool: async def _scan_calibrate_light(self) -> bool: """Request to calibration light sensitivity of Scan device.""" request = ScanLightCalibrateRequest(self._send, self._mac_in_bytes) - if (response := await request.send()) is not None: - if ( - response.node_ack_type - == NodeAckResponseType.SCAN_LIGHT_CALIBRATION_ACCEPTED - ): - self._scan_calibrate_light_scheduled = False - return True + response = await request.send() + if response is None: + _LOGGER.warning( + "No response from %s to light calibration request", + self.name, + ) return False - raise NodeTimeout( - f"No response from Scan device {self.mac} " - + "to light calibration request." + if ( + response.node_ack_type + == NodeAckResponseType.SCAN_LIGHT_CALIBRATION_ACCEPTED + ): + self._scan_calibrate_light_scheduled = False + return True + _LOGGER.warning( + "Unexpected ack type %s for light calibration on %s", + response.node_ack_type, + self.name, ) async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any]: From 46dfac20224b8485210b73d9cd61ffa1268bc433 Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 13:07:13 +0200 Subject: [PATCH 21/22] CR: Nitpick, cachewrite already stringifies internally --- plugwise_usb/nodes/scan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 71351acd1..248d6b6c9 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -475,13 +475,13 @@ async def scan_configure(self) -> bool: async def _scan_configure_update(self) -> None: """Push scan configuration update to cache.""" - self._set_cache(CACHE_SCAN_CONFIG_RESET_TIMER, str(self.reset_timer)) + self._set_cache(CACHE_SCAN_CONFIG_RESET_TIMER, self.reset_timer) self._set_cache( CACHE_SCAN_CONFIG_SENSITIVITY, self._motion_config.sensitivity_level.name, ) - self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode)) - self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty)) + self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, self.daylight_mode) + self._set_cache(CACHE_SCAN_CONFIG_DIRTY, self.dirty) await gather( self.publish_feature_update_to_subscribers( NodeFeature.MOTION_CONFIG, From b37383ac7692ca69c893ff1620534cd5a8c832bf Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sun, 24 Aug 2025 13:11:29 +0200 Subject: [PATCH 22/22] update CHANGELOG bump version to 0.44.12 --- CHANGELOG.md | 4 +++- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f34f0e54d..bc4fb4d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog -## Ongoing +## v0.44.12 - 2025-08-24 +- PR [323](https://github.com/plugwise/python-plugwise-usb/pull/323): Motion Sensitivity to use named levels (Off/Medium/High) instead of numeric values, add light sensitivity calibration on wake-up for scan devices. +- PR [322](https://github.com/plugwise/python-plugwise-usb/pull/322): Improve Circle+ load function to align to Circle load function - PR [321](https://github.com/plugwise/python-plugwise-usb/pull/321): Catch error reported in Issue [#312](https://github.com/plugwise/plugwise_usb-beta/issues/312) - PR [319](https://github.com/plugwise/python-plugwise-usb/pull/319): Replace unclear warning message when a node is not online, also various small improvements suggested by CRAI. - PR [312](https://github.com/plugwise/python-plugwise-usb/pull/312): properly propagate configuration changes and initialize to available on first node wakeup diff --git a/pyproject.toml b/pyproject.toml index 1e6f29c52..a90631a75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.44.12a2" +version = "0.44.12" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [