Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Jun 3, 2025

Summary by CodeRabbit

  • New Features

    • Added the ability to reset energy logs for supported devices.
    • Introduced a property to view node protocol versions.
  • Bug Fixes

    • Improved validation for device compatibility when setting energy intervals or resetting energy logs.
  • Documentation

    • Updated the changelog to include details for version 0.42.0.
  • Chores

    • Bumped project version to 0.42.0.

@coderabbitai
Copy link

coderabbitai bot commented Jun 3, 2025

"""

Walkthrough

This update introduces an asynchronous energy reset feature to the Plugwise USB library. It adds new methods for sending energy reset requests to nodes, enforces node-type validation for energy-related operations, and documents the changes in the changelog. The project version is incremented to 0.42.0.

Changes

File(s) Change Summary
plugwise_usb/__init__.py Added Stick.energy_reset_request() async method; minor import reordering and whitespace cleanup.
plugwise_usb/network/__init__.py Added StickNetwork.energy_reset_request() async method; added node-type checks for energy ops; updated set_energy_intervals with validation.
plugwise_usb/messages/requests.py Modified CircleClockSetRequest constructor to use LogAddr(0, 8, False) when reset is true.
plugwise_usb/nodes/node.py Added node_protocols property to PlugwiseBaseNode.
plugwise_usb/constants.py Added constants DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL, and ENERGY_NODE_TYPES.
CHANGELOG.md Added changelog entry for version 0.42.0 documenting energy reset feature.
pyproject.toml Updated project version to 0.42.0.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Stick
    participant StickNetwork
    participant Node

    User->>Stick: energy_reset_request(mac)
    Stick->>StickNetwork: energy_reset_request(mac)
    StickNetwork->>StickNetwork: _validate_energy_node(mac)
    StickNetwork->>Node: Send CircleClockSetRequest(reset=True)
    Node-->>StickNetwork: Response (acknowledgment)
    StickNetwork-->>Stick: Raise error or return success
    Stick->>StickNetwork: set_energy_intervals(mac, 60, 0)
    StickNetwork->>StickNetwork: _validate_energy_node(mac)
    StickNetwork-->>Stick: Return result or False
    Stick-->>User: Return result or False
Loading

Possibly related PRs

  • plugwise/python-plugwise-usb#247: Implements setting energy logging intervals, directly related as both PRs modify Stick and StickNetwork for energy management features.

Suggested reviewers

  • dirixmjm
  • brefra
  • ArnoutD

Poem

Hopping through code with a reset in mind,
Energy logs now leave old data behind.
Version bumped, changelog sung,
Async requests on every rung.
With protocols checked and nodes anew,
This rabbit celebrates what devs can do!
🐇✨
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ede19eb and 0202797.

📒 Files selected for processing (1)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Check commit
  • GitHub Check: Check commit

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.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

  • 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.

@bouwew bouwew changed the title Implement energy-reset functionality Implement resetting of energylogs Jun 3, 2025
@codecov
Copy link

codecov bot commented Jun 3, 2025

Codecov Report

Attention: Patch coverage is 35.89744% with 25 lines in your changes missing coverage. Please review.

Project coverage is 80.74%. Comparing base (4983462) to head (0202797).
Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
plugwise_usb/network/__init__.py 26.31% 14 Missing ⚠️
plugwise_usb/__init__.py 27.27% 8 Missing ⚠️
plugwise_usb/nodes/node.py 40.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #252      +/-   ##
==========================================
- Coverage   80.97%   80.74%   -0.24%     
==========================================
  Files          36       36              
  Lines        7418     7453      +35     
==========================================
+ Hits         6007     6018      +11     
- Misses       1411     1435      +24     

☔ 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

🧹 Nitpick comments (2)
plugwise_usb/nodes/node.py (1)

103-110: Simplify node_protocols property.

The explicit None check is redundant—returning self._node_protocols already yields None when unset. Streamline for clarity.

Proposed diff:

-    @property
-    def node_protocols(self) -> SupportedVersions | None:
-        """Return the node_protocols for the Node."""
-        if self._node_protocols is None:
-            return None
-
-        return self._node_protocols
+    @property
+    def node_protocols(self) -> SupportedVersions | None:
+        """Return supported protocol versions for this node, or None if not determined."""
+        return self._node_protocols
plugwise_usb/messages/requests.py (1)

15-15: Clarify the reason for commenting out LOGADDR_OFFSET import.

The LOGADDR_OFFSET import is commented out but it's unclear if this is a temporary change or permanent removal. If this constant is no longer needed, consider removing the import entirely rather than commenting it out.

-#    LOGADDR_OFFSET,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44947ea and 2b063cc.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • plugwise_usb/__init__.py (3 hunks)
  • plugwise_usb/messages/requests.py (2 hunks)
  • plugwise_usb/network/__init__.py (4 hunks)
  • plugwise_usb/nodes/node.py (1 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
plugwise_usb/messages/requests.py (1)
plugwise_usb/messages/properties.py (1)
  • String (80-97)
plugwise_usb/nodes/node.py (1)
plugwise_usb/nodes/helpers/firmware.py (1)
  • SupportedVersions (19-23)
🪛 Ruff (0.11.9)
plugwise_usb/messages/requests.py

767-767: Trailing whitespace

Remove trailing whitespace

(W291)

🔇 Additional comments (4)
plugwise_usb/__init__.py (2)

13-13: LGTM!

Import reordering is clean and maintains alphabetical order.


311-311: LGTM!

Good cleanup of trailing whitespace.

plugwise_usb/network/__init__.py (2)

9-9: LGTM!

Good addition of UTC import for proper timezone handling in energy reset functionality.


18-20: LGTM!

Import reordering maintains good organization and adds the necessary CircleClockSetRequest for the new energy reset feature.

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

🧹 Nitpick comments (3)
plugwise_usb/messages/requests.py (3)

15-15: Remove unused import
The constant LOGADDR_OFFSET is imported but not used anywhere in this file. Please remove it to clean up unused imports.

🧰 Tools
🪛 Ruff (0.11.9)

15-15: ..constants.LOGADDR_OFFSET imported but unused

Remove unused import: ..constants.LOGADDR_OFFSET

(F401)


767-767: Remove trailing whitespace
The line this_date, has trailing spaces. Please trim the extra whitespace.

🧰 Tools
🪛 Ruff (0.11.9)

767-767: Trailing whitespace

Remove trailing whitespace

(W291)


768-768: Introduce named constant for reset log address
Using the literal 0 in LogAddr(0, 8, False) is a magic value. Consider defining a descriptive constant (e.g., ENERGY_RESET_LOG_ADDR_INDEX = 0) at the top of the file and using it here for better readability and maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b063cc and 416adac.

📒 Files selected for processing (1)
  • plugwise_usb/messages/requests.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugwise_usb/messages/requests.py (1)
plugwise_usb/messages/properties.py (1)
  • LogAddr (384-404)
🪛 Ruff (0.11.9)
plugwise_usb/messages/requests.py

767-767: Trailing whitespace

Remove trailing whitespace

(W291)

🪛 GitHub Actions: Latest commit
plugwise_usb/messages/requests.py

[warning] 12-12: pylint: Unused LOGADDR_OFFSET imported from constants (unused-import)

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

♻️ Duplicate comments (1)
plugwise_usb/network/__init__.py (1)

548-576: ⚠️ Potential issue

Fix response attribute inconsistency in error message.

The method implementation looks good overall with proper validation, but there's an inconsistency in the error handling at the end.

Apply this diff to fix the inconsistent response attribute usage:

         if response.ack_id != NodeResponseType.CLOCK_ACCEPTED:
             raise MessageError(
-                f"Unknown NodeResponseType '{response.response_type.name}' received"
+                f"Unknown NodeResponseType '{response.ack_id.name}' received"
             )

The code checks response.ack_id but references response.response_type.name in the error message, which is inconsistent.

🧰 Tools
🪛 Ruff (0.11.9)

555-555: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🧹 Nitpick comments (1)
plugwise_usb/network/__init__.py (1)

555-555: Remove whitespace from blank line.

- 
+
🧰 Tools
🪛 Ruff (0.11.9)

555-555: Blank line contains whitespace

Remove whitespace from blank line

(W293)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17f9626 and dee5d4c.

📒 Files selected for processing (3)
  • plugwise_usb/__init__.py (3 hunks)
  • plugwise_usb/messages/requests.py (1 hunks)
  • plugwise_usb/network/__init__.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugwise_usb/messages/requests.py
  • plugwise_usb/init.py
🧰 Additional context used
🪛 Ruff (0.11.9)
plugwise_usb/network/__init__.py

555-555: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🔇 Additional comments (3)
plugwise_usb/network/__init__.py (3)

9-9: LGTM! Import additions support the new functionality.

The new imports are appropriate for the added energy reset functionality and MAC validation.

Also applies to: 16-16, 19-19, 21-21


37-38: Excellent use of constants for improved maintainability.

Defining ENERGY_NODE_TYPES as a module-level constant improves code maintainability and makes the supported node types explicit. This addresses the previous suggestion to avoid hardcoded values.


585-595: Great addition of input validation to existing method.

The addition of MAC validation, node existence checks, and node type validation to the set_energy_intervals method improves robustness and consistency with the new energy_reset_request method.

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

🧹 Nitpick comments (1)
plugwise_usb/network/__init__.py (1)

548-577: Improve error message formatting.

The method implementation is solid with proper validation and error handling. However, the error message on lines 574-576 uses string concatenation instead of a more readable f-string.

Apply this diff to improve readability:

-            raise MessageError(
-                f"Unexpected NodeResponseType {response.ack_id!r} "
-                +"received in response to CircleClockSetRequest "
-            )
+            raise MessageError(
+                f"Unexpected NodeResponseType {response.ack_id!r} received in response to CircleClockSetRequest"
+            )

Note: Static analysis indicates this new functionality lacks test coverage. Consider adding unit tests to verify the validation logic and error handling paths.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 550-551: plugwise_usb/network/init.py#L550-L551
Added lines #L550 - L551 were not covered by tests


[warning] 553-554: plugwise_usb/network/init.py#L553-L554
Added lines #L553 - L554 were not covered by tests


[warning] 556-557: plugwise_usb/network/init.py#L556-L557
Added lines #L556 - L557 were not covered by tests


[warning] 561-562: plugwise_usb/network/init.py#L561-L562
Added lines #L561 - L562 were not covered by tests


[warning] 569-570: plugwise_usb/network/init.py#L569-L570
Added lines #L569 - L570 were not covered by tests


[warning] 572-573: plugwise_usb/network/init.py#L572-L573
Added lines #L572 - L573 were not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e348b36 and 6e09f86.

📒 Files selected for processing (1)
  • plugwise_usb/network/__init__.py (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugwise_usb/network/__init__.py (5)
plugwise_usb/api.py (3)
  • NodeType (57-68)
  • mac (270-271)
  • name (242-243)
plugwise_usb/helpers/util.py (1)
  • validate_mac (13-21)
plugwise_usb/exceptions.py (2)
  • MessageError (20-21)
  • NodeError (24-25)
plugwise_usb/messages/requests.py (18)
  • CircleClockSetRequest (730-783)
  • CircleMeasureIntervalRequest (1261-1292)
  • send (364-373)
  • send (386-395)
  • send (437-439)
  • send (475-484)
  • send (511-520)
  • send (541-552)
  • send (565-574)
  • send (587-596)
  • send (620-629)
  • send (666-675)
  • send (718-727)
  • send (774-783)
  • send (807-816)
  • send (847-856)
  • send (879-888)
  • response (124-128)
plugwise_usb/__init__.py (2)
  • energy_reset_request (213-225)
  • name (141-143)
🪛 GitHub Check: codecov/patch
plugwise_usb/network/__init__.py

[warning] 550-551: plugwise_usb/network/init.py#L550-L551
Added lines #L550 - L551 were not covered by tests


[warning] 553-554: plugwise_usb/network/init.py#L553-L554
Added lines #L553 - L554 were not covered by tests


[warning] 556-557: plugwise_usb/network/init.py#L556-L557
Added lines #L556 - L557 were not covered by tests


[warning] 561-562: plugwise_usb/network/init.py#L561-L562
Added lines #L561 - L562 were not covered by tests


[warning] 569-570: plugwise_usb/network/init.py#L569-L570
Added lines #L569 - L570 were not covered by tests


[warning] 572-573: plugwise_usb/network/init.py#L572-L573
Added lines #L572 - L573 were not covered by tests


[warning] 586-587: plugwise_usb/network/init.py#L586-L587
Added lines #L586 - L587 were not covered by tests


[warning] 589-590: plugwise_usb/network/init.py#L589-L590
Added lines #L589 - L590 were not covered by tests


[warning] 592-593: plugwise_usb/network/init.py#L592-L593
Added lines #L592 - L593 were not covered by tests

🔇 Additional comments (3)
plugwise_usb/network/__init__.py (3)

9-9: LGTM! Import additions are appropriate.

All new imports (UTC, validate_mac, CircleClockSetRequest) are properly used in the new energy reset functionality.

Also applies to: 16-16, 19-19


37-37: Excellent use of constants for node type validation.

Defining ENERGY_NODE_TYPES improves code maintainability and makes the supported node types (Circle+, Circle, Stealth) explicit rather than using magic numbers.


586-595: Excellent validation consistency.

The added validation logic creates a consistent pattern with the new energy_reset_request method. Using the same ENERGY_NODE_TYPES constant and validation sequence improves code maintainability.

Note: Static analysis indicates the new validation logic lacks test coverage. Consider adding tests to verify MAC validation, node existence checks, and node type validation for both methods.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 586-587: plugwise_usb/network/init.py#L586-L587
Added lines #L586 - L587 were not covered by tests


[warning] 589-590: plugwise_usb/network/init.py#L589-L590
Added lines #L589 - L590 were not covered by tests


[warning] 592-593: plugwise_usb/network/init.py#L592-L593
Added lines #L592 - L593 were not covered by tests

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

🧹 Nitpick comments (1)
plugwise_usb/network/__init__.py (1)

548-594: Consider extracting common validation logic for better maintainability.

Both energy_reset_request and set_energy_intervals share identical validation logic for MAC format, node existence, and node type checking. Consider extracting this into a private helper method to follow the DRY principle.

def _validate_energy_node(self, mac: str) -> None:
    """Validate node for energy operations."""
    if not validate_mac(mac):
        raise NodeError(f"MAC '{mac}' invalid")
    
    if mac not in self._nodes:
        raise NodeError(f"Node {mac} not present in network")
    
    if self._nodes[mac].node_info.node_type.value not in ENERGY_NODE_TYPES:
        raise NodeError(
            f"Energy operations not supported for {self._nodes[mac].node_info.node_type.name}"
        )

Then both methods can simply call self._validate_energy_node(mac) at the beginning.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 550-551: plugwise_usb/network/init.py#L550-L551
Added lines #L550 - L551 were not covered by tests


[warning] 553-554: plugwise_usb/network/init.py#L553-L554
Added lines #L553 - L554 were not covered by tests


[warning] 556-557: plugwise_usb/network/init.py#L556-L557
Added lines #L556 - L557 were not covered by tests


[warning] 561-562: plugwise_usb/network/init.py#L561-L562
Added lines #L561 - L562 were not covered by tests


[warning] 569-570: plugwise_usb/network/init.py#L569-L570
Added lines #L569 - L570 were not covered by tests


[warning] 572-573: plugwise_usb/network/init.py#L572-L573
Added lines #L572 - L573 were not covered by tests


[warning] 585-586: plugwise_usb/network/init.py#L585-L586
Added lines #L585 - L586 were not covered by tests


[warning] 588-589: plugwise_usb/network/init.py#L588-L589
Added lines #L588 - L589 were not covered by tests


[warning] 591-592: plugwise_usb/network/init.py#L591-L592
Added lines #L591 - L592 were not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e09f86 and b5b3069.

📒 Files selected for processing (2)
  • plugwise_usb/network/__init__.py (4 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugwise_usb/network/__init__.py (6)
plugwise_usb/api.py (3)
  • NodeType (57-68)
  • mac (270-271)
  • name (242-243)
plugwise_usb/helpers/util.py (1)
  • validate_mac (13-21)
plugwise_usb/exceptions.py (2)
  • MessageError (20-21)
  • NodeError (24-25)
plugwise_usb/messages/requests.py (19)
  • CircleClockSetRequest (730-783)
  • CircleMeasureIntervalRequest (1261-1292)
  • CirclePlusAllowJoiningRequest (453-484)
  • send (364-373)
  • send (386-395)
  • send (437-439)
  • send (475-484)
  • send (511-520)
  • send (541-552)
  • send (565-574)
  • send (587-596)
  • send (620-629)
  • send (666-675)
  • send (718-727)
  • send (774-783)
  • send (807-816)
  • send (847-856)
  • send (879-888)
  • response (124-128)
plugwise_usb/__init__.py (2)
  • energy_reset_request (213-225)
  • name (141-143)
plugwise_usb/messages/responses.py (1)
  • NodeResponseType (56-72)
🪛 GitHub Check: codecov/patch
plugwise_usb/network/__init__.py

[warning] 550-551: plugwise_usb/network/init.py#L550-L551
Added lines #L550 - L551 were not covered by tests


[warning] 553-554: plugwise_usb/network/init.py#L553-L554
Added lines #L553 - L554 were not covered by tests


[warning] 556-557: plugwise_usb/network/init.py#L556-L557
Added lines #L556 - L557 were not covered by tests


[warning] 561-562: plugwise_usb/network/init.py#L561-L562
Added lines #L561 - L562 were not covered by tests


[warning] 569-570: plugwise_usb/network/init.py#L569-L570
Added lines #L569 - L570 were not covered by tests


[warning] 572-573: plugwise_usb/network/init.py#L572-L573
Added lines #L572 - L573 were not covered by tests


[warning] 585-586: plugwise_usb/network/init.py#L585-L586
Added lines #L585 - L586 were not covered by tests


[warning] 588-589: plugwise_usb/network/init.py#L588-L589
Added lines #L588 - L589 were not covered by tests


[warning] 591-592: plugwise_usb/network/init.py#L591-L592
Added lines #L591 - L592 were not covered by tests

🔇 Additional comments (4)
plugwise_usb/network/__init__.py (4)

9-9: LGTM: Import additions are correct and necessary.

The new imports (UTC, validate_mac, CircleClockSetRequest) are properly placed and required for the energy reset functionality.

Also applies to: 16-16, 19-19, 21-21


37-37: Excellent: Addresses previous review feedback.

The constant ENERGY_NODE_TYPES successfully addresses the past review comment about creating a constant for supported node types (Circle+, Circle, Stealth). This improves maintainability and makes the supported types explicit.


548-576: LGTM: Well-implemented energy reset method with proper validation.

The implementation correctly addresses all the issues raised in past reviews:

  • ✅ Uses f-string formatting properly
  • ✅ Validates MAC format before use
  • ✅ Checks node existence before access
  • ✅ Uses the new ENERGY_NODE_TYPES constant
  • ✅ Proper error handling with meaningful messages
  • ✅ Consistent response validation using response.ack_id

The method follows established patterns in the codebase and integrates well with the existing architecture.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 550-551: plugwise_usb/network/init.py#L550-L551
Added lines #L550 - L551 were not covered by tests


[warning] 553-554: plugwise_usb/network/init.py#L553-L554
Added lines #L553 - L554 were not covered by tests


[warning] 556-557: plugwise_usb/network/init.py#L556-L557
Added lines #L556 - L557 were not covered by tests


[warning] 561-562: plugwise_usb/network/init.py#L561-L562
Added lines #L561 - L562 were not covered by tests


[warning] 569-570: plugwise_usb/network/init.py#L569-L570
Added lines #L569 - L570 were not covered by tests


[warning] 572-573: plugwise_usb/network/init.py#L572-L573
Added lines #L572 - L573 were not covered by tests


585-594: LGTM: Consistent validation pattern applied.

The addition of MAC validation, node existence check, and node type validation to set_energy_intervals maintains consistency with the new energy_reset_request method. The validation logic follows the same pattern and uses the shared ENERGY_NODE_TYPES constant.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 585-586: plugwise_usb/network/init.py#L585-L586
Added lines #L585 - L586 were not covered by tests


[warning] 588-589: plugwise_usb/network/init.py#L588-L589
Added lines #L588 - L589 were not covered by tests


[warning] 591-592: plugwise_usb/network/init.py#L591-L592
Added lines #L591 - L592 were not covered by tests

@bouwew bouwew marked this pull request as ready for review June 3, 2025 10:28
@bouwew bouwew requested a review from a team as a code owner June 3, 2025 10:28
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

♻️ Duplicate comments (1)
plugwise_usb/__init__.py (1)

222-225: ⚠️ Potential issue

Fix unreachable code in return logic.

The current logic has an issue: since set_energy_intervals always returns True (line 235), the return False on line 225 is unreachable code.

Consider this fix:

 # Follow up by an energy-intervals (re)set
-if (result := await self.set_energy_intervals(mac, 60, 0)):
-    return result
-
-return False
+await self.set_energy_intervals(mac, 60, 0)
+return True

This makes the intent clearer and removes the unreachable code.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 222-223: plugwise_usb/init.py#L222-L223
Added lines #L222 - L223 were not covered by tests


[warning] 225-225: plugwise_usb/init.py#L225
Added line #L225 was not covered by tests

🧹 Nitpick comments (1)
plugwise_usb/network/__init__.py (1)

596-607: Fix whitespace issues in validation method.

The validation logic is sound, but there are whitespace issues that need to be addressed.

Apply this diff to fix the whitespace issues:

 def _validate_energy_node(self, mac: str) -> None:
     """Validate node for energy operations."""
     if not validate_mac(mac):
         raise NodeError(f"MAC '{mac}' invalid")
-        
+
     if mac not in self._nodes:
         raise NodeError(f"Node {mac} not present in network")
-        
+
     if self._nodes[mac].node_info.node_type.value not in ENERGY_NODE_TYPES:
         raise NodeError(
             f"Energy operations not supported for {self._nodes[mac].node_info.node_type.name}"
         )
🧰 Tools
🪛 Ruff (0.11.9)

600-600: Blank line contains whitespace

Remove whitespace from blank line

(W293)


603-603: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🪛 GitHub Check: codecov/patch

[warning] 598-599: plugwise_usb/network/init.py#L598-L599
Added lines #L598 - L599 were not covered by tests


[warning] 601-602: plugwise_usb/network/init.py#L601-L602
Added lines #L601 - L602 were not covered by tests


[warning] 604-605: plugwise_usb/network/init.py#L604-L605
Added lines #L604 - L605 were not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5b3069 and b0c7518.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • plugwise_usb/__init__.py (3 hunks)
  • plugwise_usb/messages/requests.py (1 hunks)
  • plugwise_usb/network/__init__.py (5 hunks)
  • plugwise_usb/nodes/node.py (1 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • pyproject.toml
  • CHANGELOG.md
  • plugwise_usb/messages/requests.py
🧰 Additional context used
🧠 Learnings (1)
plugwise_usb/__init__.py (1)
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#252
File: plugwise_usb/__init__.py:0-0
Timestamp: 2025-06-03T09:58:44.592Z
Learning: In the plugwise_usb codebase, bouwew prefers not to add excessive validation decorators or protective error handling when they would amount to "too much protection". The existing validation level is considered sufficient.
🧬 Code Graph Analysis (1)
plugwise_usb/nodes/node.py (1)
plugwise_usb/nodes/helpers/firmware.py (1)
  • SupportedVersions (19-23)
🪛 GitHub Check: codecov/patch
plugwise_usb/__init__.py

[warning] 215-219: plugwise_usb/init.py#L215-L219
Added lines #L215 - L219 were not covered by tests


[warning] 222-223: plugwise_usb/init.py#L222-L223
Added lines #L222 - L223 were not covered by tests


[warning] 225-225: plugwise_usb/init.py#L225
Added line #L225 was not covered by tests

plugwise_usb/network/__init__.py

[warning] 550-552: plugwise_usb/network/init.py#L550-L552
Added lines #L550 - L552 were not covered by tests


[warning] 559-560: plugwise_usb/network/init.py#L559-L560
Added lines #L559 - L560 were not covered by tests


[warning] 562-563: plugwise_usb/network/init.py#L562-L563
Added lines #L562 - L563 were not covered by tests


[warning] 575-575: plugwise_usb/network/init.py#L575
Added line #L575 was not covered by tests


[warning] 598-599: plugwise_usb/network/init.py#L598-L599
Added lines #L598 - L599 were not covered by tests


[warning] 601-602: plugwise_usb/network/init.py#L601-L602
Added lines #L601 - L602 were not covered by tests


[warning] 604-605: plugwise_usb/network/init.py#L604-L605
Added lines #L604 - L605 were not covered by tests

plugwise_usb/nodes/node.py

[warning] 106-107: plugwise_usb/nodes/node.py#L106-L107
Added lines #L106 - L107 were not covered by tests


[warning] 109-109: plugwise_usb/nodes/node.py#L109
Added line #L109 was not covered by tests

🪛 Ruff (0.11.9)
plugwise_usb/network/__init__.py

600-600: Blank line contains whitespace

Remove whitespace from blank line

(W293)


603-603: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🔇 Additional comments (3)
plugwise_usb/nodes/node.py (1)

103-109: LGTM! Clean property implementation.

The node_protocols property is well-implemented with proper documentation and clear logic.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 106-107: plugwise_usb/nodes/node.py#L106-L107
Added lines #L106 - L107 were not covered by tests


[warning] 109-109: plugwise_usb/nodes/node.py#L109
Added line #L109 was not covered by tests

plugwise_usb/network/__init__.py (2)

37-37: Good addition of the constant for maintainability.

The ENERGY_NODE_TYPES constant improves code maintainability by centralizing the supported node types for energy operations.


548-565: Well-implemented energy reset functionality.

The energy_reset_request method follows good patterns with proper validation, error handling, and clear logic flow.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 550-552: plugwise_usb/network/init.py#L550-L552
Added lines #L550 - L552 were not covered by tests


[warning] 559-560: plugwise_usb/network/init.py#L559-L560
Added lines #L559 - L560 were not covered by tests


[warning] 562-563: plugwise_usb/network/init.py#L562-L563
Added lines #L562 - L563 were not covered by tests

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

🧹 Nitpick comments (1)
plugwise_usb/network/__init__.py (1)

595-607: Fix whitespace issues in validation method.

The validation method logic is correct and follows good practices by centralizing validation for energy operations. However, there are whitespace issues that need to be addressed.

Apply this diff to fix the whitespace issues:

     def _validate_energy_node(self, mac: str) -> None:
         """Validate node for energy operations."""
         if not validate_mac(mac):
             raise NodeError(f"MAC '{mac}' invalid")
-        
+        
         if mac not in self._nodes:
             raise NodeError(f"Node {mac} not present in network")
-        
+        
         if self._nodes[mac].node_info.node_type.value not in ENERGY_NODE_TYPES:
             raise NodeError(
                 f"Energy operations not supported for {self._nodes[mac].node_info.node_type.name}"
             )
🧰 Tools
🪛 Ruff (0.11.9)

599-599: Blank line contains whitespace

Remove whitespace from blank line

(W293)


602-602: Blank line contains whitespace

Remove whitespace from blank line

(W293)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0c7518 and 71a8593.

📒 Files selected for processing (1)
  • plugwise_usb/network/__init__.py (5 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
plugwise_usb/network/__init__.py

599-599: Blank line contains whitespace

Remove whitespace from blank line

(W293)


602-602: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🔇 Additional comments (4)
plugwise_usb/network/__init__.py (4)

9-9: LGTM: Import additions support the new energy reset functionality.

The new imports are appropriately added to support the energy reset feature:

  • UTC from datetime for timezone-aware timestamps
  • validate_mac for MAC address validation
  • CircleClockSetRequest for the energy reset request

Also applies to: 16-16, 19-19, 21-21


37-37: Good practice: Centralized constant for supported energy node types.

The ENERGY_NODE_TYPES constant improves maintainability by centralizing the definition of supported node types (Circle, Circle+, Stealth) and makes the code more readable.


548-566: Well-implemented energy reset functionality.

The energy_reset_request method is properly implemented with:

  • Appropriate validation using the helper method
  • Correct use of CircleClockSetRequest with reset flag
  • Proper error handling and response validation
  • Clear error messages

The implementation follows the established patterns in the codebase.


575-575: Good refactor: Consistent validation across energy methods.

Adding the _validate_energy_node(mac) call to set_energy_intervals improves consistency and reduces code duplication. Both energy-related methods now use the same validation logic.

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

♻️ Duplicate comments (1)
plugwise_usb/__init__.py (1)

214-230: ⚠️ Potential issue

Fix the unreachable code and potential NoneType error.

The method has two issues:

  1. Unreachable code: The set_energy_intervals method always returns True on success (see lines 236-240), making the return False statement unreachable.

  2. Potential AttributeError: If self._network is None, calling energy_reset_request on it will raise an AttributeError.

Apply this diff to fix the logic:

 async def energy_reset_request(self, mac: str) -> bool:
     """Send an energy-reset request to a Node."""
     _LOGGER.debug("Resetting energy logs for %s", mac)
+    if self._network is None:
+        raise NodeError("Network is not initialized")
+        
     try:
         await self._network.energy_reset_request(mac)
     except (MessageError, NodeError) as exc:
         raise NodeError(f"{exc}") from exc

     # Follow up by an energy-intervals (re)set
-    if (
-        result := await self.set_energy_intervals(
-            mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
-        )
-    ):
-        return result
-
-    return False
+    try:
+        await self.set_energy_intervals(
+            mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
+        )
+        return True
+    except NodeError:
+        # Energy reset succeeded but interval setting failed
+        # Return True since the main operation succeeded
+        return True
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 216-220: plugwise_usb/init.py#L216-L220
Added lines #L216 - L220 were not covered by tests


[warning] 223-223: plugwise_usb/init.py#L223
Added line #L223 was not covered by tests


[warning] 228-228: plugwise_usb/init.py#L228
Added line #L228 was not covered by tests


[warning] 230-230: plugwise_usb/init.py#L230
Added line #L230 was not covered by tests

🧹 Nitpick comments (1)
plugwise_usb/__init__.py (1)

214-230: Add test coverage for the new energy reset functionality.

The static analysis indicates that the new energy_reset_request method lacks test coverage, which is important for ensuring the energy reset functionality works correctly.

Would you like me to help generate unit tests for this new method to improve the test coverage?

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 216-220: plugwise_usb/init.py#L216-L220
Added lines #L216 - L220 were not covered by tests


[warning] 223-223: plugwise_usb/init.py#L223
Added line #L223 was not covered by tests


[warning] 228-228: plugwise_usb/init.py#L228
Added line #L228 was not covered by tests


[warning] 230-230: plugwise_usb/init.py#L230
Added line #L230 was not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae404a and ede19eb.

📒 Files selected for processing (1)
  • plugwise_usb/__init__.py (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
plugwise_usb/__init__.py (1)
Learnt from: bouwew
PR: plugwise/python-plugwise-usb#252
File: plugwise_usb/__init__.py:0-0
Timestamp: 2025-06-03T09:58:44.592Z
Learning: In the plugwise_usb codebase, bouwew prefers not to add excessive validation decorators or protective error handling when they would amount to "too much protection". The existing validation level is considered sufficient.
🪛 GitHub Check: codecov/patch
plugwise_usb/__init__.py

[warning] 216-220: plugwise_usb/init.py#L216-L220
Added lines #L216 - L220 were not covered by tests


[warning] 223-223: plugwise_usb/init.py#L223
Added line #L223 was not covered by tests


[warning] 228-228: plugwise_usb/init.py#L228
Added line #L228 was not covered by tests


[warning] 230-230: plugwise_usb/init.py#L230
Added line #L230 was not covered by tests

🔇 Additional comments (2)
plugwise_usb/__init__.py (2)

13-13: LGTM: Import reorganization looks good.

The typing imports reorganization and addition of the new constants are appropriate for the new functionality.

Also applies to: 17-17


317-317: Good cleanup: Trailing whitespace removal.

Good practice to remove trailing whitespace.

@bouwew
Copy link
Contributor Author

bouwew commented Jun 3, 2025

@coderabbitai please suggest test code for the added lines of code.

@coderabbitai
Copy link

coderabbitai bot commented Jun 3, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@bouwew I'll analyze the existing test structure and suggest comprehensive test code for the new energy reset functionality. Let me first examine the current test patterns.

Based on the codebase analysis, here are comprehensive test suggestions for the new energy reset functionality:

Test Code Suggestions

1. Tests for Stick.energy_reset_request() method

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from plugwise_usb import Stick
from plugwise_usb.exceptions import MessageError, NodeError
from plugwise_usb.constants import DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL

class TestStickEnergyReset:
    @pytest.fixture
    def stick(self):
        """Create a Stick instance for testing."""
        stick = Stick()
        stick._network = AsyncMock()
        return stick

    @pytest.mark.asyncio
    async def test_energy_reset_request_success(self, stick):
        """Test successful energy reset request."""
        mac = "000D6F0000A1B2C3"
        
        # Mock the network calls to succeed
        stick._network.energy_reset_request = AsyncMock()
        stick.set_energy_intervals = AsyncMock(return_value=True)
        
        result = await stick.energy_reset_request(mac)
        
        # Verify the calls were made correctly
        stick._network.energy_reset_request.assert_called_once_with(mac)
        stick.set_energy_intervals.assert_called_once_with(
            mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
        )
        assert result is True

    @pytest.mark.asyncio
    async def test_energy_reset_request_set_intervals_fails(self, stick):
        """Test energy reset when set_energy_intervals returns falsy value."""
        mac = "000D6F0000A1B2C3"
        
        stick._network.energy_reset_request = AsyncMock()
        stick.set_energy_intervals = AsyncMock(return_value=False)
        
        result = await stick.energy_reset_request(mac)
        
        stick._network.energy_reset_request.assert_called_once_with(mac)
        stick.set_energy_intervals.assert_called_once_with(
            mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
        )
        assert result is False

    @pytest.mark.asyncio
    async def test_energy_reset_request_message_error(self, stick):
        """Test energy reset request handles MessageError."""
        mac = "000D6F0000A1B2C3"
        
        stick._network.energy_reset_request = AsyncMock(side_effect=MessageError("Test message error"))
        
        with pytest.raises(NodeError):
            await stick.energy_reset_request(mac)
        
        stick._network.energy_reset_request.assert_called_once_with(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_node_error(self, stick):
        """Test energy reset request handles NodeError."""
        mac = "000D6F0000A1B2C3"
        
        stick._network.energy_reset_request = AsyncMock(side_effect=NodeError("Test node error"))
        
        with pytest.raises(NodeError):
            await stick.energy_reset_request(mac)
        
        stick._network.energy_reset_request.assert_called_once_with(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_set_intervals_exception(self, stick):
        """Test energy reset when set_energy_intervals raises exception."""
        mac = "000D6F0000A1B2C3"
        
        stick._network.energy_reset_request = AsyncMock()
        stick.set_energy_intervals = AsyncMock(side_effect=NodeError("Intervals error"))
        
        result = await stick.energy_reset_request(mac)
        
        # Should return False when set_energy_intervals fails
        assert result is False

2. Tests for StickNetwork.energy_reset_request() method

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
from plugwise_usb.network import StickNetwork
from plugwise_usb.exceptions import MessageError, NodeError
from plugwise_usb.messages.requests import CircleClockSetRequest
from plugwise_usb.constants import CLOCK_ACCEPTED

class TestStickNetworkEnergyReset:
    @pytest.fixture
    def network(self):
        """Create a StickNetwork instance for testing."""
        network = StickNetwork()
        network._validate_energy_node = MagicMock()
        network._send_request = AsyncMock()
        return network

    @pytest.mark.asyncio
    async def test_energy_reset_request_success(self, network):
        """Test successful energy reset request."""
        mac = "000D6F0000A1B2C3"
        
        # Mock successful validation and response
        mock_response = MagicMock()
        mock_response.acknowledge_id = CLOCK_ACCEPTED
        network._send_request.return_value = mock_response
        
        # Mock node protocols
        with patch.object(network, '_nodes', {mac: MagicMock(node_protocols=MagicMock(max_protocol=2))}):
            await network.energy_reset_request(mac)
        
        # Verify validation was called
        network._validate_energy_node.assert_called_once_with(mac)
        
        # Verify request was sent
        network._send_request.assert_called_once()
        args = network._send_request.call_args[0]
        assert isinstance(args[0], CircleClockSetRequest)

    @pytest.mark.asyncio
    async def test_energy_reset_request_no_response(self, network):
        """Test energy reset request when no response received."""
        mac = "000D6F0000A1B2C3"
        
        network._send_request.return_value = None
        
        with patch.object(network, '_nodes', {mac: MagicMock(node_protocols=MagicMock(max_protocol=2))}):
            with pytest.raises(NodeError, match="No response received"):
                await network.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_wrong_ack(self, network):
        """Test energy reset request with wrong acknowledgment ID."""
        mac = "000D6F0000A1B2C3"
        
        mock_response = MagicMock()
        mock_response.acknowledge_id = "WRONG_ACK"
        network._send_request.return_value = mock_response
        
        with patch.object(network, '_nodes', {mac: MagicMock(node_protocols=MagicMock(max_protocol=2))}):
            with pytest.raises(MessageError):
                await network.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_validation_error(self, network):
        """Test energy reset request when validation fails."""
        mac = "000D6F0000A1B2C3"
        
        network._validate_energy_node.side_effect = NodeError("Invalid node")
        
        with pytest.raises(NodeError, match="Invalid node"):
            await network.energy_reset_request(mac)

3. Tests for StickNetwork._validate_energy_node() method

import pytest
from unittest.mock import MagicMock, patch
from plugwise_usb.network import StickNetwork
from plugwise_usb.exceptions import NodeError
from plugwise_usb.constants import ENERGY_NODE_TYPES

class TestStickNetworkValidation:
    @pytest.fixture
    def network(self):
        """Create a StickNetwork instance for testing."""
        return StickNetwork()

    def test_validate_energy_node_success(self, network):
        """Test successful energy node validation."""
        mac = "000D6F0000A1B2C3"
        
        # Mock valid node
        mock_node = MagicMock()
        mock_node.type = 1  # Assuming 1 is in ENERGY_NODE_TYPES
        
        with patch('plugwise_usb.network.validate_mac') as mock_validate_mac:
            with patch.object(network, '_nodes', {mac: mock_node}):
                network._validate_energy_node(mac)
        
        mock_validate_mac.assert_called_once_with(mac)

    def test_validate_energy_node_invalid_mac(self, network):
        """Test validation with invalid MAC address."""
        mac = "invalid_mac"
        
        with patch('plugwise_usb.network.validate_mac', side_effect=ValueError("Invalid MAC")):
            with pytest.raises(NodeError, match="Invalid MAC"):
                network._validate_energy_node(mac)

    def test_validate_energy_node_not_present(self, network):
        """Test validation when node is not present."""
        mac = "000D6F0000A1B2C3"
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_nodes', {}):
                with pytest.raises(NodeError, match="not present"):
                    network._validate_energy_node(mac)

    def test_validate_energy_node_wrong_type(self, network):
        """Test validation when node type is not allowed."""
        mac = "000D6F0000A1B2C3"
        
        # Mock node with invalid type
        mock_node = MagicMock()
        mock_node.type = 99  # Assuming 99 is not in ENERGY_NODE_TYPES
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_nodes', {mac: mock_node}):
                with pytest.raises(NodeError, match="not included in the allowed energy node types"):
                    network._validate_energy_node(mac)

    @pytest.mark.parametrize("node_type", [1, 2, 9])  # Based on ENERGY_NODE_TYPES
    def test_validate_energy_node_valid_types(self, network, node_type):
        """Test validation with all valid energy node types."""
        mac = "000D6F0000A1B2C3"
        
        mock_node = MagicMock()
        mock_node.type = node_type
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_nodes', {mac: mock_node}):
                # Should not raise any exception
                network._validate_energy_node(mac)

4. Tests for PlugwiseBaseNode.node_protocols property

import pytest
from plugwise_usb.nodes.node import PlugwiseBaseNode

class TestPlugwiseBaseNodeProtocols:
    def test_node_protocols_with_value(self):
        """Test node_protocols property when _node_protocols is set."""
        node = PlugwiseBaseNode()
        mock_protocols = "test_protocols"
        node._node_protocols = mock_protocols
        
        assert node.node_protocols == mock_protocols

    def test_node_protocols_none(self):
        """Test node_protocols property when _node_protocols is None."""
        node = PlugwiseBaseNode()
        node._node_protocols = None
        
        assert node.node_protocols is None

    def test_node_protocols_unset(self):
        """Test node_protocols property when _node_protocols is not set."""
        node = PlugwiseBaseNode()
        # Don't set _node_protocols attribute
        
        assert node.node_protocols is None

5. Tests for modified CircleClockSetRequest

import pytest
from plugwise_usb.messages.requests import CircleClockSetRequest
from plugwise_usb.messages.types import LogAddr

class TestCircleClockSetRequestReset:
    def test_circle_clock_set_request_with_reset_true(self):
        """Test CircleClockSetRequest constructor when reset=True."""
        from datetime import datetime, timezone
        
        now = datetime.now(timezone.utc)
        mac = "000D6F0000A1B2C3"
        protocols = 2
        
        request = CircleClockSetRequest(mac, now, protocols, reset=True)
        
        # Verify that LogAddr was set to (0, 8, False) when reset=True
        # This test depends on the internal structure, may need adjustment
        # based on actual implementation details
        assert hasattr(request, 'log_addr') or hasattr(request, '_log_addr')

    def test_circle_clock_set_request_with_reset_false(self):
        """Test CircleClockSetRequest constructor when reset=False."""
        from datetime import datetime, timezone
        
        now = datetime.now(timezone.utc)
        mac = "000D6F0000A1B2C3"
        protocols = 2
        
        request = CircleClockSetRequest(mac, now, protocols, reset=False)
        
        # Verify normal behavior when reset=False
        assert hasattr(request, 'log_addr') or hasattr(request, '_log_addr')

6. Integration test for the complete flow

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from plugwise_usb import Stick
from plugwise_usb.constants import DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL, CLOCK_ACCEPTED

class TestEnergyResetIntegration:
    @pytest.mark.asyncio
    async def test_full_energy_reset_flow(self):
        """Test the complete energy reset flow from Stick to StickNetwork."""
        stick = Stick()
        mac = "000D6F0000A1B2C3"
        
        # Mock all the dependencies
        with patch.object(stick, '_network') as mock_network:
            with patch.object(stick, 'set_energy_intervals', return_value=True) as mock_set_intervals:
                # Mock successful network call
                mock_network.energy_reset_request = AsyncMock()
                
                result = await stick.energy_reset_request(mac)
                
                # Verify the full flow
                mock_network.energy_reset_request.assert_called_once_with(mac)
                mock_set_intervals.assert_called_once_with(
                    mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
                )
                assert result is True

These comprehensive tests cover:

  • Happy path scenarios for all new methods
  • Error handling (MessageError, NodeError exceptions)
  • Edge cases (missing nodes, wrong node types, invalid MACs)
  • Property behavior for the new node_protocols property
  • Modified constructor logic for CircleClockSetRequest
  • Integration testing for the complete flow
  • Parameterized tests for different valid node types

The tests use pytest async features, mocking, and parametrization to ensure comprehensive coverage of the new energy reset functionality. This should significantly improve the patch coverage from the current 35.89%.


🏁 Script executed:

#!/bin/bash
# Find and examine existing test files to understand the testing patterns
fd "test.*\.py$" --type f | head -10

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Look for test files specifically related to the modules that were changed
fd "test.*\.py$" --type f | xargs grep -l -E "(Stick|StickNetwork|PlugwiseBaseNode|CircleClockSetRequest)" | head -5

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Examine the structure of existing tests for the Stick class
find . -name "test*.py" -exec grep -l "class.*Test.*Stick\|def test.*stick\|def test.*energy" {} \; | head -3

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Get a sample of existing test file to understand the testing structure
fd "test.*\.py$" --type f | head -1 | xargs cat | head -50

Length of output: 1663


🏁 Script executed:

#!/bin/bash
# Look for existing test directories and their structure
find . -type d -name "*test*" | head -10

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check for pytest or unittest patterns in the repository
rg "import pytest|import unittest|from unittest|@pytest\." --type py | head -10

Length of output: 524


🏁 Script executed:

#!/bin/bash
# Look at the overall test directory structure
find . -name "test*" -type f | head -15

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Examine one of the main test files to understand imports and setup
fd "test.*\.py$" --type f | head -1 | xargs head -30

Length of output: 991


Now I have a better understanding of the existing test structure. Let me provide test code suggestions that match the project's testing patterns:

Test Code Suggestions for New Energy Reset Functionality

Based on the existing test structure in tests/test_usb.py, here are comprehensive test suggestions that follow the project's patterns:

Add to tests/test_usb.py

# Add these test methods to the existing TestStick class

class TestStick:
    # ... existing tests ...

    @pytest.mark.asyncio
    async def test_energy_reset_request_success(self):
        """Test successful energy reset request."""
        mac = "000D6F0000A1B2C3"
        stick = Stick("dummy")
        
        # Mock the network and set_energy_intervals method
        stick._network = Mock()
        stick._network.energy_reset_request = Mock()
        
        with patch.object(stick, 'set_energy_intervals', return_value=True) as mock_intervals:
            result = await stick.energy_reset_request(mac)
            
            # Verify calls were made
            stick._network.energy_reset_request.assert_called_once_with(mac)
            mock_intervals.assert_called_once_with(mac, 60, 0)  # DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
            assert result is True

    @pytest.mark.asyncio
    async def test_energy_reset_request_intervals_fail(self):
        """Test energy reset when setting intervals fails."""
        mac = "000D6F0000A1B2C3"
        stick = Stick("dummy")
        
        stick._network = Mock()
        stick._network.energy_reset_request = Mock()
        
        with patch.object(stick, 'set_energy_intervals', return_value=False) as mock_intervals:
            result = await stick.energy_reset_request(mac)
            
            stick._network.energy_reset_request.assert_called_once_with(mac)
            mock_intervals.assert_called_once_with(mac, 60, 0)
            assert result is False

    @pytest.mark.asyncio
    async def test_energy_reset_request_message_error(self):
        """Test energy reset handles MessageError."""
        from plugwise_usb.exceptions import MessageError, NodeError
        
        mac = "000D6F0000A1B2C3"
        stick = Stick("dummy")
        
        stick._network = Mock()
        stick._network.energy_reset_request = Mock(side_effect=MessageError("Test error"))
        
        with pytest.raises(NodeError):
            await stick.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_node_error(self):
        """Test energy reset re-raises NodeError."""
        from plugwise_usb.exceptions import NodeError
        
        mac = "000D6F0000A1B2C3"
        stick = Stick("dummy")
        
        stick._network = Mock()
        stick._network.energy_reset_request = Mock(side_effect=NodeError("Test error"))
        
        with pytest.raises(NodeError):
            await stick.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_energy_reset_request_intervals_exception(self):
        """Test energy reset when set_energy_intervals raises exception."""
        from plugwise_usb.exceptions import NodeError
        
        mac = "000D6F0000A1B2C3"
        stick = Stick("dummy")
        
        stick._network = Mock()
        stick._network.energy_reset_request = Mock()
        
        with patch.object(stick, 'set_energy_intervals', side_effect=NodeError("Intervals error")):
            result = await stick.energy_reset_request(mac)
            assert result is False

Add to tests/test_usb.py for StickNetwork tests

# Add these test methods to test StickNetwork functionality

class TestStickNetwork:
    """Test class for StickNetwork energy reset functionality."""

    @pytest.mark.asyncio
    async def test_network_energy_reset_request_success(self):
        """Test successful network energy reset request."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.constants import CLOCK_ACCEPTED
        from datetime import datetime, timezone
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        # Mock node with valid type
        mock_node = Mock()
        mock_node.type = 1  # Valid energy node type
        mock_node.node_protocols = Mock()
        mock_node.node_protocols.max_protocol = 2
        
        network._nodes = {mac: mock_node}
        
        # Mock successful response
        mock_response = Mock()
        mock_response.acknowledge_id = CLOCK_ACCEPTED
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_send_request', return_value=mock_response):
                await network.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_network_energy_reset_request_no_response(self):
        """Test network energy reset when no response received."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.exceptions import NodeError
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        mock_node = Mock()
        mock_node.type = 1
        mock_node.node_protocols = Mock()
        mock_node.node_protocols.max_protocol = 2
        
        network._nodes = {mac: mock_node}
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_send_request', return_value=None):
                with pytest.raises(NodeError, match="No response received"):
                    await network.energy_reset_request(mac)

    @pytest.mark.asyncio
    async def test_network_energy_reset_request_wrong_ack(self):
        """Test network energy reset with wrong acknowledgment."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.exceptions import MessageError
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        mock_node = Mock()
        mock_node.type = 1
        mock_node.node_protocols = Mock()
        mock_node.node_protocols.max_protocol = 2
        
        network._nodes = {mac: mock_node}
        
        mock_response = Mock()
        mock_response.acknowledge_id = "WRONG_ACK"
        
        with patch('plugwise_usb.network.validate_mac'):
            with patch.object(network, '_send_request', return_value=mock_response):
                with pytest.raises(MessageError):
                    await network.energy_reset_request(mac)

    def test_validate_energy_node_success(self):
        """Test successful energy node validation."""
        from plugwise_usb.network import StickNetwork
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        mock_node = Mock()
        mock_node.type = 1  # Valid energy node type
        network._nodes = {mac: mock_node}
        
        with patch('plugwise_usb.network.validate_mac'):
            # Should not raise any exception
            network._validate_energy_node(mac)

    def test_validate_energy_node_invalid_mac(self):
        """Test validation with invalid MAC address."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.exceptions import NodeError
        
        mac = "invalid_mac"
        network = StickNetwork()
        
        with patch('plugwise_usb.network.validate_mac', side_effect=ValueError("Invalid MAC")):
            with pytest.raises(NodeError, match="Invalid MAC"):
                network._validate_energy_node(mac)

    def test_validate_energy_node_not_present(self):
        """Test validation when node is not present."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.exceptions import NodeError
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        network._nodes = {}
        
        with patch('plugwise_usb.network.validate_mac'):
            with pytest.raises(NodeError, match="not present"):
                network._validate_energy_node(mac)

    def test_validate_energy_node_wrong_type(self):
        """Test validation when node type is not allowed."""
        from plugwise_usb.network import StickNetwork
        from plugwise_usb.exceptions import NodeError
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        mock_node = Mock()
        mock_node.type = 99  # Invalid energy node type
        network._nodes = {mac: mock_node}
        
        with patch('plugwise_usb.network.validate_mac'):
            with pytest.raises(NodeError, match="not included in the allowed energy node types"):
                network._validate_energy_node(mac)

    @pytest.mark.parametrize("node_type", [1, 2, 9])
    def test_validate_energy_node_valid_types(self, node_type):
        """Test validation with all valid energy node types."""
        from plugwise_usb.network import StickNetwork
        
        mac = "000D6F0000A1B2C3"
        network = StickNetwork()
        
        mock_node = Mock()
        mock_node.type = node_type
        network._nodes = {mac: mock_node}
        
        with patch('plugwise_usb.network.validate_mac'):
            # Should not raise any exception
            network._validate_energy_node(mac)

Add to tests/test_usb.py for node protocols property

class TestPlugwiseBaseNode:
    """Test class for PlugwiseBaseNode node_protocols property."""

    def test_node_protocols_with_value(self):
        """Test node_protocols property when _node_protocols is set."""
        from plugwise_usb.nodes.node import PlugwiseBaseNode
        
        node = PlugwiseBaseNode()
        mock_protocols = Mock()
        node._node_protocols = mock_protocols
        
        assert node.node_protocols == mock_protocols

    def test_node_protocols_none(self):
        """Test node_protocols property when _node_protocols is None."""
        from plugwise_usb.nodes.node import PlugwiseBaseNode
        
        node = PlugwiseBaseNode()
        node._node_protocols = None
        
        assert node.node_protocols is None

    def test_node_protocols_unset(self):
        """Test node_protocols property when _node_protocols is not set."""
        from plugwise_usb.nodes.node import PlugwiseBaseNode
        
        node = PlugwiseBaseNode()
        # Don't set _node_protocols attribute
        
        assert node.node_protocols is None

Add to tests/test_usb.py for CircleClockSetRequest modification

class TestCircleClockSetRequest:
    """Test class for CircleClockSetRequest reset functionality."""

    def test_circle_clock_set_request_with_reset_true(self):
        """Test CircleClockSetRequest constructor when reset=True."""
        from plugwise_usb.messages.requests import CircleClockSetRequest
        from datetime import datetime, timezone
        
        now = datetime.now(timezone.utc)
        mac = "000D6F0000A1B2C3"
        protocols = 2
        
        # Test that no exception is raised when reset=True
        request = CircleClockSetRequest(mac, now, protocols, reset=True)
        assert request is not None

    def test_circle_clock_set_request_with_reset_false(self):
        """Test CircleClockSetRequest constructor when reset=False (default)."""
        from plugwise_usb.messages.requests import CircleClockSetRequest
        from datetime import datetime, timezone
        
        now = datetime.now(timezone.utc)
        mac = "000D6F0000A1B2C3"
        protocols = 2
        
        # Test that no exception is raised when reset=False (default behavior)
        request = CircleClockSetRequest(mac, now, protocols, reset=False)
        assert request is not None

Add response message data to tests/stick_test_data.py

# Add to the RESPONSE_MESSAGES dictionary in stick_test_data.py

# Energy reset request success response
b"\x05\x05\x03\x03[energy_reset_request_hex]\r\n": (
    "Energy Reset Success",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for clock set response
    + b"000D6F0000A1B2C3"  # mac
    + b"00AC",  # CLOCK_ACCEPTED acknowledgment
),

# Energy reset request failure response  
b"\x05\x05\x03\x03[energy_reset_failure_hex]\r\n": (
    "Energy Reset Failure",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for clock set response
    + b"000D6F0000A1B2C3"  # mac
    + b"00C1",  # Different acknowledgment (failure)
),

Key Testing Considerations

  1. Pattern Matching: The tests follow the existing project patterns using pytest, Mock objects, and async testing.

  2. Coverage Areas: These tests cover:

    • ✅ Happy path scenarios for energy reset
    • ✅ Error handling (MessageError, NodeError)
    • ✅ Node validation logic
    • ✅ Property behavior
    • ✅ Modified constructor logic
  3. Mocking Strategy: Uses the same mocking approach as existing tests with unittest.mock.Mock and patch.

  4. Test Data: Follows the pattern of using predefined test data from stick_test_data.py.

  5. Async Testing: Uses @pytest.mark.asyncio decorator matching existing async test patterns.

These tests should significantly improve the patch coverage from 35.89% to cover the 25 missing lines identified by codecov. The tests are designed to integrate seamlessly with the existing test suite structure.


🏁 Script executed:

#!/bin/bash
# Examine the main test file to understand the testing patterns and structure
cat tests/test_usb.py

Length of output: 65560


Now I have a complete understanding of the test structure. Based on the existing patterns in tests/test_usb.py, here are the test code suggestions that match the project's testing style:

Test Code Suggestions for Energy Reset Functionality

1. Add to the existing TestStick class in tests/test_usb.py

@pytest.mark.asyncio
async def test_energy_reset_request_success(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test successful energy reset request."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection", 
        mock_serial.mock_connection,
    )
    monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2)
    monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0)
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load the circle plus node for testing
    assert await stick.nodes["0098765432101234"].load()
    
    # Test energy reset request
    result = await stick.energy_reset_request("0098765432101234")
    assert result is True
    
    await stick.disconnect()

@pytest.mark.asyncio
async def test_energy_reset_request_invalid_node(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test energy reset request with invalid node type."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Try energy reset on Scan node (invalid energy node type)
    with pytest.raises(pw_exceptions.NodeError):
        await stick.energy_reset_request("5555555555555555")
    
    await stick.disconnect()

@pytest.mark.asyncio
async def test_energy_reset_request_node_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test energy reset request with non-existent node."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    
    # Try energy reset on non-existent node
    with pytest.raises(pw_exceptions.NodeError):
        await stick.energy_reset_request("AAAAAAAAAAAAAAAA")
    
    await stick.disconnect()

@pytest.mark.asyncio 
async def test_energy_reset_request_set_intervals_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test energy reset when set_energy_intervals fails."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load the circle plus node
    assert await stick.nodes["0098765432101234"].load()
    
    # Mock set_energy_intervals to return False
    async def mock_set_energy_intervals(mac: str, consumption: int, production: int) -> bool:
        return False
    
    monkeypatch.setattr(stick, "set_energy_intervals", mock_set_energy_intervals)
    
    result = await stick.energy_reset_request("0098765432101234")
    assert result is False
    
    await stick.disconnect()

@pytest.mark.asyncio
async def test_energy_reset_request_message_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test energy reset handles MessageError from network."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load the circle plus node
    assert await stick.nodes["0098765432101234"].load()
    
    # Mock network to raise MessageError
    async def mock_energy_reset_request(mac: str) -> None:
        raise pw_exceptions.MessageError("Test message error")
    
    monkeypatch.setattr(stick._network, "energy_reset_request", mock_energy_reset_request)
    
    with pytest.raises(pw_exceptions.NodeError):
        await stick.energy_reset_request("0098765432101234")
    
    await stick.disconnect()

2. Add test for node_protocols property

def test_node_protocols_property(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test node_protocols property on PlugwiseBaseNode."""
    # Test with protocols set
    node = pw_node.PlugwiseBaseNode()
    mock_protocols = Mock()
    node._node_protocols = mock_protocols
    assert node.node_protocols == mock_protocols
    
    # Test with protocols as None
    node._node_protocols = None
    assert node.node_protocols is None
    
    # Test with protocols not set
    node_empty = pw_node.PlugwiseBaseNode()
    assert node_empty.node_protocols is None

3. Add to tests/stick_test_data.py - Response messages for energy reset

# Add these entries to the RESPONSE_MESSAGES dictionary

# Energy reset request response (CircleClockSetRequest with reset=True)
b"\x05\x05\x03\x030060000D6F0000A1B2C3626268000000BEEF\r\n": (
    "Energy Reset Request",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for CircleClockSetResponse
    + b"000D6F0000A1B2C3"  # mac
    + b"00AC",  # CLOCK_ACCEPTED acknowledgment
),

# Energy reset request failure response
b"\x05\x05\x03\x030060000D6F0000A1B2C3626268000000DEAD\r\n": (
    "Energy Reset Request Failure", 
    b"000000C1",  # Success ack
    b"0061"  # msg_id for CircleClockSetResponse
    + b"000D6F0000A1B2C3"  # mac
    + b"00C1",  # Different acknowledgment (failure)
),

# Set energy intervals request after reset
b"\x05\x05\x03\x03004C000D6F0000A1B2C3003C0000ABCD\r\n": (
    "Set Energy Intervals After Reset",
    b"000000C1",  # Success ack
    b"004D"  # msg_id for CircleEnergyIntervalResponse  
    + b"000D6F0000A1B2C3"  # mac
    + b"00AC",  # Success acknowledgment
),

4. Add comprehensive network-level tests

@pytest.mark.asyncio
async def test_network_energy_reset_validation(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test StickNetwork energy reset validation."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Test validation with invalid MAC
    with pytest.raises(pw_exceptions.NodeError):
        await stick._network.energy_reset_request("invalid_mac")
    
    # Test validation with non-existent node
    with pytest.raises(pw_exceptions.NodeError):
        await stick._network.energy_reset_request("AAAAAAAAAAAAAAAA")
    
    # Test validation with wrong node type (Scan node)
    # First ensure the scan node is discovered
    mock_serial.inject_message(b"004F555555555555555500", b"FFFE")
    await asyncio.sleep(0.1)  # Allow time for node discovery
    
    with pytest.raises(pw_exceptions.NodeError):
        await stick._network.energy_reset_request("5555555555555555")
    
    await stick.disconnect()

@pytest.mark.asyncio
async def test_set_energy_intervals_with_validation(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that set_energy_intervals now includes validation."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load valid energy node
    assert await stick.nodes["0098765432101234"].load()
    
    # Test successful validation and intervals setting
    await stick.set_energy_intervals("0098765432101234", 60, 0)
    
    # Test validation failure with wrong node type
    mock_serial.inject_message(b"004F555555555555555500", b"FFFE")
    await asyncio.sleep(0.1)
    
    with pytest.raises(pw_exceptions.NodeError):
        await stick.set_energy_intervals("5555555555555555", 60, 0)
    
    await stick.disconnect()

5. Add CircleClockSetRequest modification test

def test_circle_clock_set_request_reset_parameter(self) -> None:
    """Test CircleClockSetRequest with reset parameter."""
    from datetime import datetime, timezone
    
    # Test that reset parameter doesn't cause errors
    now = datetime.now(timezone.utc)
    mac = b"000D6F0000A1B2C3"
    protocols = 2
    
    # Test with reset=True
    request_reset = pw_requests.CircleClockSetRequest(
        self.dummy_fn, mac, now, protocols, reset=True
    )
    assert request_reset is not None
    
    # Test with reset=False (default)
    request_normal = pw_requests.CircleClockSetRequest(
        self.dummy_fn, mac, now, protocols, reset=False
    )
    assert request_normal is not None
    
    # Test without reset parameter (should default to False)
    request_default = pw_requests.CircleClockSetRequest(
        self.dummy_fn, mac, now, protocols
    )
    assert request_default is not None

6. Add integration test for complete energy reset flow

@pytest.mark.asyncio
async def test_complete_energy_reset_flow(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test complete energy reset flow from Stick to Network."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.2)
    monkeypatch.setattr(pw_requests, "NODE_TIME_OUT", 1.0)
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load energy node
    assert await stick.nodes["0098765432101234"].load()
    
    # Verify node has energy features and correct type
    assert pw_api.NodeFeature.ENERGY in stick.nodes["0098765432101234"].features
    assert stick.nodes["0098765432101234"].type in pw_constants.ENERGY_NODE_TYPES
    
    # Test complete energy reset flow
    result = await stick.energy_reset_request("0098765432101234")
    assert result is True
    
    await stick.disconnect()

7. Add error handling tests for edge cases

@pytest.mark.asyncio
async def test_energy_reset_request_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test energy reset request timeout handling."""
    mock_serial = MockSerial(
        {
            # Mock timeout response for energy reset
            b"\x05\x05\x03\x030060000D6F0000A1B2C3626268000000": (
                "Energy Reset Timeout",
                b"000000E1",  # Timeout ack
                None,
            ),
        }
    )
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    monkeypatch.setattr(pw_sender, "STICK_TIME_OUT", 0.1)
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load valid energy node
    assert await stick.nodes["0098765432101234"].load()
    
    # Test should handle timeout gracefully
    with pytest.raises(pw_exceptions.NodeError):
        await stick.energy_reset_request("0098765432101234")
    
    await stick.disconnect()

@pytest.mark.asyncio
async def test_energy_reset_constants_usage(self, monkeypatch: pytest.MonkeyPatch) -> None:
    """Test that energy reset uses correct constants."""
    mock_serial = MockSerial(None)
    monkeypatch.setattr(
        pw_connection_manager,
        "create_serial_connection",
        mock_serial.mock_connection,
    )
    
    stick = pw_stick.Stick("test_port", cache_enabled=False)
    await stick.connect()
    await stick.initialize()
    await stick.discover_nodes(load=False)
    
    # Load valid energy node
    assert await stick.nodes["0098765432101234"].load()
    
    # Mock set_energy_intervals to capture the parameters
    captured_params = {}
    async def mock_set_energy_intervals(mac: str, consumption: int, production: int) -> bool:
        captured_params['mac'] = mac
        captured_params['consumption'] = consumption
        captured_params['production'] = production
        return True
    
    monkeypatch.setattr(stick, "set_energy_intervals", mock_set_energy_intervals)
    
    await stick.energy_reset_request("0098765432101234")
    
    # Verify correct constants are used
    assert captured_params['consumption'] == pw_constants.DEFAULT_CONS_INTERVAL  # 60
    assert captured_params['production'] == pw_constants.NO_PRODUCTION_INTERVAL  # 0
    
    await stick.disconnect()

Key Test Coverage Points

These comprehensive tests cover:

Happy path scenarios - Successful energy reset with proper flow
Error handling - MessageError, NodeError, timeouts
Validation logic - Invalid MACs, wrong node types, missing nodes
Integration testing - Complete flow from Stick to StickNetwork
Constants usage - Proper use of DEFAULT_CONS_INTERVAL and NO_PRODUCTION_INTERVAL
Property behavior - node_protocols property functionality
Request modification - CircleClockSetRequest reset parameter
Edge cases - Timeouts, communication failures, validation errors

The tests follow the existing project patterns:

  • Use pytest.mark.asyncio for async tests
  • Use MockSerial for communication simulation
  • Use monkeypatch for dependency injection
  • Follow the existing naming conventions and class structure
  • Include proper setup/teardown with connect/disconnect
  • Use the same import patterns with importlib.import_module

This should significantly improve the patch coverage from 35.89% to cover most of the 25 missing lines identified by codecov.

@bouwew
Copy link
Contributor Author

bouwew commented Jun 3, 2025

@coderabbitai please correct the code to add to stick_test_data.py to using 1111111111111111 as mac address.
Also, Energy reset request response (CircleClockSetRequest with reset=True) should start with the b"0016" _identifier.
And, set energy intervals request after reset, should start with the b"0057" _identifier.

@coderabbitai
Copy link

coderabbitai bot commented Jun 3, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@bouwew I'll examine the current stick_test_data.py structure and provide the corrected test data with your specifications.

Based on the test data structure, here's the corrected code for tests/stick_test_data.py:

Corrected Test Data for tests/stick_test_data.py

Add these entries to the RESPONSE_MESSAGES dictionary:

# Energy reset request response (CircleClockSetRequest with reset=True)
b"\x05\x05\x03\x03001611111111111111110000000000000000BEEF\r\n": (
    "Energy Reset Request",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for CircleClockSetResponse
    + b"1111111111111111"  # mac
    + b"00AC",  # CLOCK_ACCEPTED acknowledgment
),

# Energy reset request failure response
b"\x05\x05\x03\x03001611111111111111110000000000000000DEAD\r\n": (
    "Energy Reset Request Failure",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for CircleClockSetResponse
    + b"1111111111111111"  # mac
    + b"00C1",  # Different acknowledgment (failure)
),

# Set energy intervals request after reset  
b"\x05\x05\x03\x0300571111111111111111003C0000ABCD\r\n": (
    "Set Energy Intervals After Reset",
    b"000000C1",  # Success ack
    b"004D"  # msg_id for CircleEnergyIntervalResponse
    + b"1111111111111111"  # mac
    + b"00AC",  # Success acknowledgment
),

The key corrections made:

  • ✅ Changed MAC address to 1111111111111111 as requested
  • ✅ Energy reset request now starts with b"0016" identifier
  • ✅ Set energy intervals request now starts with b"0057" identifier
  • ✅ Maintained the proper hex encoding format used in the existing test data
  • ✅ Kept the response message structure consistent with existing patterns

These test data entries should now properly support testing the energy reset functionality with the correct identifiers and MAC address format.


🏁 Script executed:

#!/bin/bash
# Examine the structure of stick_test_data.py to understand the format
cat tests/stick_test_data.py | head -50

Length of output: 1644


🏁 Script executed:

#!/bin/bash
# Look for examples of identifiers 0016 and 0057 in the test data
grep -A 5 -B 5 "0016\|0057" tests/stick_test_data.py

Length of output: 1265


@bouwew Thank you for the clarification! Now I can see the correct structure from the existing test data. I notice that identifier 0016 is already used for "Clock set" operations in the PARTLY_RESPONSE_MESSAGES dictionary, and there's already an entry for MAC 1111111111111111.

Here's the corrected code for tests/stick_test_data.py:

Corrected Test Data for tests/stick_test_data.py

Add to the RESPONSE_MESSAGES dictionary:

# Energy reset request (CircleClockSetRequest with reset=True) - complete message
b"\x05\x05\x03\x03001611111111111111110000000000000000BEEF\r\n": (
    "Energy Reset Request Complete",
    b"000000C1",  # Success ack
    b"0061"  # msg_id for CircleClockSetResponse
    + b"1111111111111111"  # mac
    + b"00D7",  # CLOCK_ACCEPTED acknowledgment (matching existing pattern)
),

# Set energy intervals request after reset
b"\x05\x05\x03\x0300571111111111111111003C0000ABCD\r\n": (
    "Set Energy Intervals After Reset",
    b"000000C1",  # Success ack
    b"004D"  # msg_id for CircleEnergyIntervalResponse
    + b"1111111111111111"  # mac
    + b"00AC",  # Success acknowledgment
),

The existing entry in PARTLY_RESPONSE_MESSAGES already covers the partial match:

b"\x05\x05\x03\x0300161111111111111111": (
    "Clock set 1111111111111111",
    b"000000C1",  # Success ack
    b"0000" + b"00D7" + b"1111111111111111",  # msg_id, ClockAccepted, mac
),

Key corrections made:

  • ✅ Using MAC address 1111111111111111 as requested
  • ✅ Energy reset requests use identifier 0016 (already exists in PARTLY_RESPONSE_MESSAGES)
  • ✅ Set energy intervals requests use identifier 0057
  • ✅ Using b"00D7" for CLOCK_ACCEPTED to match existing test data pattern
  • ✅ Maintaining the proper hex encoding format and response structure

The existing PARTLY_RESPONSE_MESSAGES entry for 0016 with MAC 1111111111111111 will handle partial matches for energy reset requests, while the new RESPONSE_MESSAGES entries will handle complete message scenarios.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jun 4, 2025

@bouwew bouwew merged commit f7276eb into main Jun 4, 2025
27 of 29 checks passed
@bouwew bouwew deleted the energy-reset branch June 4, 2025 09:11
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.

2 participants