Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Aug 22, 2025

Summary by CodeRabbit

  • New Features
    • Scan device light calibration is now scheduled and runs automatically when the device is awake for improved reliability.
  • Improvements
    • Motion sensitivity uses named enum levels instead of raw numbers for clearer configuration.
    • Sensitivity loading and caching improved for more stable behavior across restarts; cached sensitivity stored as enum name.
  • Refactor
    • Calibration flow and sensitivity handling rewritten to use scheduled execution and public properties.
  • Tests
    • Unit tests updated to use enum sensitivity values.
  • Chores
    • Bumped version to 0.44.12a2.

@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

Motion sensitivity moved from integers to the MotionSensitivity enum across API, messages, node logic and cache; Scan light calibration was changed to a scheduled flow executed during awake tasks (public call now schedules); tests updated to use the enum; project version bumped to 0.44.12a2.

Changes

Cohort / File(s) Summary
API: MotionConfig
plugwise_usb/api.py
MotionConfig.sensitivity_level type changed from `int
Scan node: scheduling, config & cache
plugwise_usb/nodes/scan.py
Added _scan_calibrate_light_scheduled flag and scheduling flow; scan_calibrate_light() now schedules (returns None); implemented _scan_calibrate_light() to send ScanLightCalibrateRequest during _run_awake_tasks; set_motion_sensitivity_level and sensitivity_level now use MotionSensitivity; cache load/store records enum names.
Messages: request typing & payload
plugwise_usb/messages/requests.py
ScanConfigureRequest.__init__ sensitivity parameter now accepts MotionSensitivity; payload encodes sensitivity.value (enum underlying int) instead of raw int.
Tests: enum usage
tests/test_usb.py
Replaced numeric sensitivity literals with pw_api.MotionSensitivity enum members (e.g., HIGH, MEDIUM) across sensitivity-related tests.
Project metadata
pyproject.toml
Bumped project version from 0.44.12a1 to 0.44.12a2.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant PlugwiseScan
  participant AwakeLoop as "Awake Tasks Loop"
  participant Device as "Scan Device"

  Caller->>PlugwiseScan: scan_calibrate_light()  (schedule)
  Note right of PlugwiseScan #f9f0c1: set _scan_calibrate_light_scheduled = True
  PlugwiseScan-->>Caller: return None

  AwakeLoop->>PlugwiseScan: _run_awake_tasks()
  alt scheduled
    PlugwiseScan->>PlugwiseScan: _scan_calibrate_light()
    PlugwiseScan->>Device: ScanLightCalibrateRequest
    alt Accepted
      Device-->>PlugwiseScan: Accepted
      Note right of PlugwiseScan #cfe8d4: clear schedule flag
    else Denied
      Device-->>PlugwiseScan: Denied
      Note right of PlugwiseScan #fce8d6: keep/clear flag per result
    else Timeout
      Device--x PlugwiseScan: no response (NodeTimeout)
    end
  else not scheduled
    Note right of AwakeLoop #eef3ff: continue other tasks
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit taps the schedule drum,
“Calibrate later—when awake we’ll come!”
Enums hop in, numbers take their rest,
Cache keeps names — the code’s new quest.
Version nudged ahead — a tiny, happy thrum 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mdi_scan_bouwew

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Aug 22, 2025

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.45%. Comparing base (fa33be0) to head (8072bd3).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
plugwise_usb/nodes/scan.py 76.92% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #325      +/-   ##
==========================================
- Coverage   80.57%   80.45%   -0.12%     
==========================================
  Files          36       36              
  Lines        7566     7574       +8     
==========================================
- Hits         6096     6094       -2     
- Misses       1470     1480      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugwise_usb/nodes/scan.py (2)

201-209: Make cache loader robust to both legacy names and numeric values

_sensitivity_level_from_cache assumes uppercase enum names (e.g., "MEDIUM"). With the new plan to store numbers, this will KeyError on "30"/"20"/"255". Support both:

-    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
-        """Load sensitivity level from cache."""
-        if (
-            sensitivity_level := self._get_cache(
-                CACHE_SCAN_CONFIG_SENSITIVITY
-            )  # returns level in string CAPITALS
-        ) is not None:
-            return MotionSensitivity[sensitivity_level]
-        return None
+    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
+        """Load sensitivity level from cache (accepts legacy names and numeric values)."""
+        if (raw := self._get_cache(CACHE_SCAN_CONFIG_SENSITIVITY)) is None:
+            return None
+        # Legacy: "HIGH" / "MEDIUM" / "OFF"
+        try:
+            return MotionSensitivity[raw]
+        except KeyError:
+            pass
+        # New: "20" / "30" / "255"
+        try:
+            return MotionSensitivity(int(raw))
+        except (ValueError, KeyError):
+            _LOGGER.warning(
+                "Invalid cached sensitivity '%s' for %s, defaulting to %s",
+                raw,
+                self.name,
+                DEFAULT_SENSITIVITY.name,
+            )
+            return DEFAULT_SENSITIVITY

329-346: Operate with enums internally; convert to value only at the boundary

Given MotionConfig.sensitivity_level should be MotionSensitivity, adapt comparisons, logging, and storage accordingly:

-    async def set_motion_sensitivity_level(self, level: MotionSensitivity) -> 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,
-            level.value,
+            level,
         )
-        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()
         return True
🧹 Nitpick comments (4)
plugwise_usb/api.py (1)

681-687: Nit: docstring grammar

“Request to calibration …” → “Request to calibrate …” if you decide to keep the old semantics.

tests/test_usb.py (1)

2245-2253: Enum-based sensitivity tests LGTM; consider adding cache-backward-compat coverage

These checks are fine. Given the code aims to store numeric sensitivity in cache, add a parametrized test to verify both legacy (“MEDIUM”, “HIGH”, “OFF”) and numeric (“30”, “20”, “255”) cache values are accepted and mapped to the same enum.

I can draft an additional test function that monkeypatches _get_cache to return each variant and asserts the resulting MotionConfig.sensitivity_level is MotionSensitivity.MEDIUM/HIGH/OFF.

plugwise_usb/nodes/scan.py (2)

429-431: Awake task: handle calibrate exceptions to avoid aborting the awake cycle

_node timeout during _scan_calibrate_light will raise NodeTimeout. Consider catching/logging to keep the awake-task pipeline resilient:

-        if self._scan_calibrate_light_scheduled:
-            await self._scan_calibrate_light()
+        if self._scan_calibrate_light_scheduled:
+            try:
+                await self._scan_calibrate_light()
+            except NodeTimeout as exc:
+                _LOGGER.warning("Light calibration timed out for %s: %s", self.name, exc)

If you prefer to retry automatically, we can keep the scheduled flag set on timeout to retry on the next awake cycle (current behavior).


505-506: Re-try policy on failure: clarify behavior

You only clear the scheduled flag on SCAN_LIGHT_CALIBRATION_ACCEPTED. On failure, the flag stays set, so the next awake cycle will retry. If that’s intentional, mention it in the docstring of scan_calibrate_light() or add a short comment here for maintainers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fa33be0 and 5e93f32.

📒 Files selected for processing (4)
  • plugwise_usb/api.py (2 hunks)
  • plugwise_usb/nodes/scan.py (8 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_usb.py (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T17:50:29.188Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#302
File: plugwise_usb/nodes/scan.py:132-142
Timestamp: 2025-08-07T17:50:29.188Z
Learning: In the plugwise_usb codebase, the `_load_defaults` method in node classes (scan.py, switch.py, sense.py) is only called once during node initialization, not during runtime. Therefore, flags like `_sed_node_info_update_task_scheduled` don't need to be reset at the start of `_load_defaults` as there's no risk of stale state from previous loads.

Applied to files:

  • plugwise_usb/nodes/scan.py
🧬 Code graph analysis (3)
plugwise_usb/api.py (2)
plugwise_usb/nodes/scan.py (4)
  • daylight_mode (234-238)
  • reset_timer (270-274)
  • sensitivity_level (277-281)
  • scan_calibrate_light (493-495)
plugwise_usb/nodes/node.py (1)
  • scan_calibrate_light (369-371)
tests/test_usb.py (3)
plugwise_usb/nodes/scan.py (3)
  • set_motion_sensitivity_level (329-345)
  • motion_config (260-267)
  • dirty (229-231)
plugwise_usb/api.py (3)
  • set_motion_sensitivity_level (662-679)
  • MotionSensitivity (22-27)
  • motion_config (498-508)
plugwise_usb/nodes/node.py (2)
  • set_motion_sensitivity_level (790-796)
  • motion_config (249-255)
plugwise_usb/nodes/scan.py (4)
plugwise_usb/api.py (4)
  • MotionSensitivity (22-27)
  • set_motion_sensitivity_level (662-679)
  • name (288-289)
  • scan_calibrate_light (681-686)
plugwise_usb/nodes/node.py (4)
  • set_motion_sensitivity_level (790-796)
  • name (218-222)
  • _set_cache (689-703)
  • scan_calibrate_light (369-371)
plugwise_usb/messages/requests.py (17)
  • ScanLightCalibrateRequest (1407-1425)
  • response (124-128)
  • send (357-366)
  • send (379-388)
  • send (430-432)
  • send (468-477)
  • send (504-513)
  • send (534-545)
  • send (558-567)
  • send (580-589)
  • send (613-622)
  • send (659-668)
  • send (711-720)
  • send (767-776)
  • send (800-809)
  • send (840-849)
  • send (872-881)
plugwise_usb/messages/responses.py (1)
  • NodeAckResponseType (75-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (3)
pyproject.toml (1)

7-7: Version bump looks good

No issues spotted with the version update to 0.44.12a2. Please ensure a matching changelog entry and tag are created when moving this PR out of draft.

Would you like me to draft a brief CHANGELOG section for 0.44.12a2?

tests/test_usb.py (1)

1837-1838: Enum usage in tests looks good

Switching to pw_api.MotionSensitivity in these tests is consistent with the updated setter signature and increases readability.

Also applies to: 1868-1869, 1895-1896

plugwise_usb/nodes/scan.py (1)

89-89: Scheduling flag initialization: good addition

_init sets _scan_calibrate_light_scheduled = False. This integrates cleanly with the awake-task flow.

Comment on lines +236 to 244
sensitivity_level: MotionSensitivity | None: Motion sensitivity level.
dirty: bool: Settings changed, device update pending
"""

daylight_mode: bool | None = None
reset_timer: int | None = None
sensitivity_level: int | None = None
sensitivity_level: MotionSensitivity | None = None
dirty: bool = False
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type contract drift: MotionConfig.sensitivity_level vs implementation

You’ve changed MotionConfig.sensitivity_level to MotionSensitivity | None, which is a good direction. However, PlugwiseScan currently stores and exposes an int for sensitivity_level (see nodes/scan.py line 168 and property at line 276), leading to a mismatch with this public type. This will trip strict type-checking and confuse downstream consumers.

Recommend making the node consistently use MotionSensitivity end-to-end (store the enum in _motion_config, property returns MotionSensitivity) and only convert to the numeric value at the request boundary and when writing to cache. See suggested fixes in scan.py comments.

🤖 Prompt for AI Agents
In plugwise_usb/api.py around lines 236 to 244, MotionConfig.sensitivity_level
is now typed as MotionSensitivity | None but the node implementation stores and
exposes an int; update the node to store the MotionSensitivity enum in its
internal _motion_config and make the public property return MotionSensitivity
(or None) instead of int; when persisting to cache or preparing network requests
convert the enum to its numeric value (e.g., enum.value) and when reading from
cache or responses convert numeric values back into MotionSensitivity using
MotionSensitivity(value) to maintain a consistent enum-based contract
end-to-end.

Comment on lines +681 to +687
async def scan_calibrate_light(self) -> bool:
"""Request to calibration light sensitivity of Scan device.
Description:
Request to calibration light sensitivity of Scan device.
"""

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

API contract mismatch: scan_calibrate_light return type

In this Protocol, scan_calibrate_light() returns bool with semantics “True if successful.” In PlugwiseScan (nodes/scan.py lines 493-496), the method now schedules calibration and returns None, with execution moved to _scan_calibrate_light() during awake tasks.

This is a breaking contract between the public API (Protocol and base class) and the concrete implementation. Please align the API to the new scheduling model. Suggested change: make the public method “schedule-only” and return None consistently across API, base class, and implementations; document that actual success/failure will be surfaced via logs/events.

Proposed diff for this file:

-    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 scan_calibrate_light(self) -> None:
+        """Schedule light sensitivity calibration of a Scan device.
+
+        Description:
+            Schedules a light sensitivity calibration request to be performed
+            when the device is awake for maintenance. No return value.
+        """

Also update nodes/node.py to match (see scan.py review for a code block).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 scan_calibrate_light(self) -> None:
"""Schedule light sensitivity calibration of a Scan device.
Description:
Schedules a light sensitivity calibration request to be performed
when the device is awake for maintenance. No return value.
"""

Comment on lines +493 to +496
async def scan_calibrate_light(self) -> None:
"""Schedule light sensitivity calibration of Scan device."""
self._scan_calibrate_light_scheduled = True

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Public API semantics changed; propagate to base class and Protocol

This method now schedules calibration and returns None. Please update:

  • PlugwiseNode Protocol (api.py) to -> None with a scheduling docstring (see api.py comment).
  • PlugwiseBaseNode (nodes/node.py) signature and NotImplementedError docstring accordingly.

You can keep _scan_calibrate_light() as the boolean-returning executor.

🤖 Prompt for AI Agents
In plugwise_usb/api.py and plugwise_usb/nodes/node.py (update the PlugwiseNode
Protocol and PlugwiseBaseNode method definitions respectively), change the
signature of scan_calibrate_light to async def scan_calibrate_light(self) ->
None and update the docstring to state this method schedules light sensitivity
calibration (does not run it) and returns None; in PlugwiseBaseNode adjust the
NotImplementedError text to instruct implementers to schedule calibration and
return None (keep the existing _scan_calibrate_light executor that returns a
boolean unchanged).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
plugwise_usb/nodes/scan.py (3)

201-210: Cache reader will KeyError on numeric writes; accept both numeric and legacy enum-name forms

As written, if the cache contains "30" (written by current _scan_configure_update), MotionSensitivity["30"] raises KeyError during load. Make the reader dual-format and resilient.

-    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
-        """Load sensitivity level from cache."""
-        if (
-            sensitivity_level := self._get_cache(
-                CACHE_SCAN_CONFIG_SENSITIVITY
-            )  # returns level in string CAPITALS
-        ) is not None:
-            return MotionSensitivity[sensitivity_level]
-        return None
+    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
+        """Load sensitivity level from cache.
+
+        Accepts either:
+        - numeric string/int (e.g. '20', '30', '255'), or
+        - legacy enum name (e.g. 'HIGH', 'MEDIUM', 'OFF').
+        """
+        raw = self._get_cache(CACHE_SCAN_CONFIG_SENSITIVITY)
+        if raw is None:
+            return None
+        # Prefer numeric interpretation first
+        try:
+            if isinstance(raw, int) or (isinstance(raw, str) and raw.isdigit()):
+                return MotionSensitivity(int(raw))
+        except (ValueError, KeyError):
+            pass
+        # Fallback to enum name
+        if isinstance(raw, str):
+            try:
+                return MotionSensitivity[raw]
+            except KeyError:
+                _LOGGER.warning(
+                    "Unsupported cached sensitivity '%s' for %s, defaulting to %s",
+                    raw,
+                    self.name,
+                    DEFAULT_SENSITIVITY.name,
+                )
+                return DEFAULT_SENSITIVITY
+        return None

277-282: Return MotionSensitivity from the property (aligns with public API)

Keep the enum internally and expose it. Convert to int only at external boundaries (requests, cache). This also makes MotionConfig() constructed in motion_config type-correct.

-    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.value
+    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

329-345: Accept and store the enum directly; fix comparison and logging

Store level (enum) instead of its numeric value and compare against the enum to avoid accidental int/enum drift. Keep conversion at the request/cache boundaries.

 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,
-        level.value,
+        level,
     )
-    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()
     return True
♻️ Duplicate comments (4)
plugwise_usb/nodes/scan.py (4)

445-454: Convert enum to numeric only when building the request

If you adopt the enum-internal model, pass .value at the boundary. Keeps internal types consistent and the wire format numeric.

         request = ScanConfigureRequest(
             self._send,
             self._mac_in_bytes,
-            self.reset_timer,
-            self.sensitivity_level,
-            self.daylight_mode,
+            self.reset_timer,
+            self.sensitivity_level.value,
+            self.daylight_mode,
         )

493-496: Public API semantics changed; align Protocol/base signatures and docs

This method now “schedules” calibration and returns None. Update the PlugwiseNode Protocol and PlugwiseBaseNode accordingly so downstream users don’t expect a bool.

Run to spot signature mismatches:

#!/bin/bash
# Find public scan_calibrate_light declarations/usages that still expect a bool
rg -nP -C2 --type=py '\bscan_calibrate_light\s*\('

I can help prepare coordinated changes in api.py and nodes/node.py if you want.


165-170: Store MotionSensitivity in MotionConfig (not its int value)

The MotionConfig type (in api.py) now expects MotionSensitivity | None. Storing level.value here makes the public surface inconsistent and cascades into cache/write mismatches later.

Apply:

         self._motion_config = MotionConfig(
             daylight_mode=daylight_mode,
             reset_timer=reset_timer,
-            sensitivity_level=sensitivity_level.value,
+            sensitivity_level=sensitivity_level,
             dirty=dirty,
         )

476-485: Cache: persist numeric sensitivity (stringified) for stability and dual-read support

Writing the enum or a bare int leads to fragile reloads. Persist a numeric string; the updated reader accepts both numeric and legacy names.

-        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._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_RESET_TIMER, str(self.reset_timer))
+        self._set_cache(
+            CACHE_SCAN_CONFIG_SENSITIVITY,
+            str((self._motion_config.sensitivity_level or DEFAULT_SENSITIVITY).value),
+        )
+        self._set_cache(CACHE_SCAN_CONFIG_DAYLIGHT_MODE, str(self.daylight_mode))
+        self._set_cache(CACHE_SCAN_CONFIG_DIRTY, str(self.dirty))
🧹 Nitpick comments (1)
plugwise_usb/nodes/scan.py (1)

497-512: Optional: log negative ACK for calibration

Currently a non-accepted ACK returns False silently. A warning with the ack type helps diagnose why a device refused calibration.

         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
+            _LOGGER.warning(
+                "Light calibration not accepted for %s (ack: %s)",
+                self.name,
+                response.node_ack_type,
+            )
             return False
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2987192 and a78207d.

📒 Files selected for processing (4)
  • plugwise_usb/api.py (2 hunks)
  • plugwise_usb/nodes/scan.py (9 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_usb.py (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_usb.py
  • plugwise_usb/api.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T17:50:29.188Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#302
File: plugwise_usb/nodes/scan.py:132-142
Timestamp: 2025-08-07T17:50:29.188Z
Learning: In the plugwise_usb codebase, the `_load_defaults` method in node classes (scan.py, switch.py, sense.py) is only called once during node initialization, not during runtime. Therefore, flags like `_sed_node_info_update_task_scheduled` don't need to be reset at the start of `_load_defaults` as there's no risk of stale state from previous loads.

Applied to files:

  • plugwise_usb/nodes/scan.py
🧬 Code graph analysis (1)
plugwise_usb/nodes/scan.py (4)
plugwise_usb/api.py (4)
  • MotionSensitivity (22-27)
  • set_motion_sensitivity_level (662-679)
  • name (288-289)
  • scan_calibrate_light (681-686)
plugwise_usb/nodes/node.py (4)
  • set_motion_sensitivity_level (790-796)
  • name (218-222)
  • _set_cache (689-703)
  • scan_calibrate_light (369-371)
plugwise_usb/messages/requests.py (17)
  • ScanLightCalibrateRequest (1407-1425)
  • response (124-128)
  • send (357-366)
  • send (379-388)
  • send (430-432)
  • send (468-477)
  • send (504-513)
  • send (534-545)
  • send (558-567)
  • send (580-589)
  • send (613-622)
  • send (659-668)
  • send (711-720)
  • send (767-776)
  • send (800-809)
  • send (840-849)
  • send (872-881)
plugwise_usb/messages/responses.py (2)
  • node_ack_type (918-920)
  • NodeAckResponseType (75-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (2)
plugwise_usb/nodes/scan.py (2)

87-94: Good addition: explicit scheduling flag for light calibration

The dedicated _scan_calibrate_light_scheduled flag makes the awake-task flow clear and avoids immediate I/O from public API calls.


429-431: Nice: calibration runs during awake tasks

Triggering _scan_calibrate_light() from _run_awake_tasks() when scheduled matches the rest of the SED scheduling model.

@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugwise_usb/nodes/scan.py (1)

201-209: Backward-compat cache read: handle legacy numeric values and invalid data

_sensitivity_level_from_cache assumes enum names (e.g., "MEDIUM"). Existing caches likely contain numeric strings ("20", "30", "255"). MotionSensitivity["30"] raises KeyError -> load failure. Parse both forms and be robust to garbage with a warning.

Apply this diff:

-    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
-        """Load sensitivity level from cache."""
-        if (
-            sensitivity_level := self._get_cache(
-                CACHE_SCAN_CONFIG_SENSITIVITY
-            )  # returns level in string CAPITALS
-        ) is not None:
-            return MotionSensitivity[sensitivity_level]
-        return None
+    def _sensitivity_level_from_cache(self) -> MotionSensitivity | None:
+        """Load sensitivity level from cache.
+
+        Accepts both enum name (e.g., 'MEDIUM') and legacy numeric string (e.g., '30')."""
+        raw = self._get_cache(CACHE_SCAN_CONFIG_SENSITIVITY)
+        if raw is None:
+            return None
+        # Try enum name
+        try:
+            return MotionSensitivity[raw]
+        except KeyError:
+            pass
+        # Try numeric
+        try:
+            return MotionSensitivity(int(raw))
+        except (ValueError, KeyError):
+            _LOGGER.warning(
+                "Unexpected cached sensitivity '%s' for %s; falling back to default",
+                raw,
+                self.mac,
+            )
+            return None

Please add/adjust a unit test to cover both cache formats.

♻️ Duplicate comments (2)
plugwise_usb/nodes/scan.py (2)

478-485: Cache write uses enum name; consider storing numeric for stability (while keeping dual-read)

Writing the enum name is readable, but names can be refactored; numeric values are the protocol constants. With the dual-read fix above, you can safely keep names, but consider persisting the numeric value for long-term stability.

Optional tweak:

-        self._set_cache(
-            CACHE_SCAN_CONFIG_SENSITIVITY,
-            self._motion_config.sensitivity_level.name,
-        )
+        self._set_cache(
+            CACHE_SCAN_CONFIG_SENSITIVITY,
+            str(self._motion_config.sensitivity_level.value),
+        )

If you prefer names, keep as-is, since the reader will now accept both.


493-506: Public API behavior changed (scheduling, returns None) but Protocol/base still declare -> bool

scan_calibrate_light now schedules and returns None, but PlugwiseNode Protocol in api.py still advertises -> bool (see provided snippet), and PlugwiseBaseNode likely mirrors that. This will confuse callers and type checkers.

Proposed follow-ups (outside this file):

  • In plugwise_usb/api.py Protocol: change signature to -> None and update docstring to state it schedules calibration.
  • In plugwise_usb/nodes/node.py base class: update abstract/NotImplementedError method signature and docstring accordingly.
  • Ensure any callers/tests expecting a bool are adapted.

Example updates (outside this file):

# plugwise_usb/api.py
class PlugwiseNode(Protocol):
    async def scan_calibrate_light(self) -> None:
        """Schedule light sensitivity calibration of the Scan device (returns immediately)."""
# plugwise_usb/nodes/node.py
class PlugwiseBaseNode:
    async def scan_calibrate_light(self) -> None:
        """Schedule light sensitivity calibration; implementers should enqueue and return None."""
        raise NotImplementedError()

Please also add a brief release note about the behavioral change.

Run this to spot mismatches:

#!/bin/bash
# Find all scan_calibrate_light definitions and references
rg -nP -C2 '\bscan_calibrate_light\s*\('

# Inspect cache key usage/format across codebase
rg -nP -C2 'CACHE_SCAN_CONFIG_SENSITIVITY'
🧹 Nitpick comments (3)
plugwise_usb/messages/requests.py (2)

12-12: Consider dependency layering: messages now import API enum

Bringing MotionSensitivity in from ..api couples low-level message building to the high-level API module. If api.py ever depends on messages, you risk circular imports. Consider relocating MotionSensitivity to a neutral types/enums module (e.g., plugwise_usb/types.py) consumed by both api and messages.


1375-1382: Accept both enum and legacy int; validate early and fail fast

The signature now requires MotionSensitivity and you convert via sensitivity.value. That’s good for type safety, but it breaks any legacy call sites/tests that still pass an int. To be resilient, accept MotionSensitivity | int and normalize to the enum with a clear error on invalid values.

Apply this diff:

-    def __init__(
+    def __init__(
         self,
         send_fn: Callable[[PlugwiseRequest, bool], Awaitable[PlugwiseResponse | None]],
         mac: bytes,
         reset_timer: int,
-        sensitivity: MotionSensitivity,
+        sensitivity: MotionSensitivity | int,
         light: bool,
     ):
         """Initialize ScanConfigureRequest message object."""
         super().__init__(send_fn, mac)
         reset_timer_value = Int(reset_timer, length=2)
         # Sensitivity: HIGH(0x14),  MEDIUM(0x1E),  OFF(0xFF)
-        sensitivity_value = Int(sensitivity.value, length=2)
+        if not isinstance(sensitivity, MotionSensitivity):
+            try:
+                sensitivity = MotionSensitivity(int(sensitivity))
+            except (ValueError, TypeError) as exc:
+                raise MessageError(f"Invalid Scan sensitivity: {sensitivity!r}") from exc
+        sensitivity_value = Int(sensitivity.value, length=2)
         light_temp = 1 if light else 0

Also consider adding a short note to the class docstring that the parameter accepts MotionSensitivity (preferred) and legacy ints 20/30/255.

Also applies to: 1386-1394

plugwise_usb/nodes/scan.py (1)

329-336: Guard logging against None to avoid AttributeError on first-use

If _motion_config.sensitivity_level is None (e.g., very early before cache/defaults applied), .name access will raise. Minor, but easy to harden logging.

Apply this diff:

         _LOGGER.debug(
             "set_motion_sensitivity_level | Device %s | %s -> %s",
             self.name,
-            self._motion_config.sensitivity_level.name,
-            level.name,
+            getattr(self._motion_config.sensitivity_level, "name", "None"),
+            level.name,
         )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a78207d and 8072bd3.

📒 Files selected for processing (3)
  • plugwise_usb/messages/requests.py (2 hunks)
  • plugwise_usb/nodes/scan.py (8 hunks)
  • tests/test_usb.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_usb.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T17:50:29.188Z
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#302
File: plugwise_usb/nodes/scan.py:132-142
Timestamp: 2025-08-07T17:50:29.188Z
Learning: In the plugwise_usb codebase, the `_load_defaults` method in node classes (scan.py, switch.py, sense.py) is only called once during node initialization, not during runtime. Therefore, flags like `_sed_node_info_update_task_scheduled` don't need to be reset at the start of `_load_defaults` as there's no risk of stale state from previous loads.

Applied to files:

  • plugwise_usb/nodes/scan.py
🧬 Code graph analysis (2)
plugwise_usb/messages/requests.py (2)
plugwise_usb/api.py (1)
  • MotionSensitivity (22-27)
plugwise_usb/messages/properties.py (14)
  • Int (103-129)
  • value (76-80)
  • value (96-100)
  • value (125-129)
  • value (159-163)
  • value (187-191)
  • value (207-211)
  • value (251-255)
  • value (276-280)
  • value (301-305)
  • value (330-334)
  • value (359-363)
  • value (380-384)
  • value (403-407)
plugwise_usb/nodes/scan.py (4)
plugwise_usb/api.py (4)
  • MotionSensitivity (22-27)
  • set_motion_sensitivity_level (662-679)
  • name (288-289)
  • scan_calibrate_light (681-686)
plugwise_usb/nodes/node.py (4)
  • set_motion_sensitivity_level (790-796)
  • name (218-222)
  • _set_cache (689-703)
  • scan_calibrate_light (369-371)
plugwise_usb/messages/requests.py (17)
  • ScanLightCalibrateRequest (1408-1426)
  • response (125-129)
  • send (358-367)
  • send (380-389)
  • send (431-433)
  • send (469-478)
  • send (505-514)
  • send (535-546)
  • send (559-568)
  • send (581-590)
  • send (614-623)
  • send (660-669)
  • send (712-721)
  • send (768-777)
  • send (801-810)
  • send (841-850)
  • send (873-882)
plugwise_usb/messages/responses.py (2)
  • node_ack_type (918-920)
  • NodeAckResponseType (75-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (4)
plugwise_usb/nodes/scan.py (4)

87-92: Scheduling flag for light calibration: good addition

The dedicated _scan_calibrate_light_scheduled flag cleanly separates scheduling from execution. No issues spotted.


277-282: Property now returns MotionSensitivity: consistent with API

Returning the enum aligns with the updated API and reduces magic numbers in callers. LGTM.


429-431: Awake task executes scheduled calibration: good integration

Executing the calibration opportunistically during awake time is sensible for SED nodes. LGTM.


451-454: Passing enum into ScanConfigureRequest matches new request API

Using self.sensitivity_level (enum) is now correct given the request converts to .value.

@bouwew
Copy link
Contributor Author

bouwew commented Aug 24, 2025

Closing because #323 will be merged instead.

@bouwew bouwew closed this Aug 24, 2025
@bouwew bouwew deleted the mdi_scan_bouwew branch August 24, 2025 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants