feat: Add device capability system and advanced control commands#52
feat: Add device capability system and advanced control commands#52
Conversation
- Implement device capability checking with DeviceCapabilityChecker class - Add device info caching with configurable update intervals via DeviceInfoCache - Introduce @requires_capability decorator for automatic capability validation - Add DeviceCapabilityError exception for unsupported features Advanced MQTT Control Commands: - Demand response: enable_demand_response(), disable_demand_response() - Air filter maintenance: reset_air_filter() - Vacation mode: set_vacation_days() - Water program reservations: configure_reservation_water_program() - Recirculation control: set_recirculation_mode(), configure_recirculation_schedule(), trigger_recirculation_hot_button() Enhancements: - CLI with new diagnostics capabilities - Documentation for device control and capabilities - Example scripts for new features - Type hint improvements for optional parameters - Export new classes and decorators from main package
- Remove sensitive device identifiers (MAC addresses) from log messages - Addresses GitHub Advanced Security code scanning alerts about clear-text logging of sensitive data - Fixes 7 security code scanning issues in command_decorators.py, mqtt_client.py, and mqtt_subscriptions.py - Maintains all functionality while improving security posture
- Remove examples/mqtt_diagnostics_output from version control - Add examples/mqtt_diagnostics_output/ to .gitignore to prevent future tracking
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive device capability system for the nwp500 library, enabling automatic validation of device features before executing MQTT control commands. The changes introduce three core components: a DeviceCapabilityChecker for mapping features to capabilities, a DeviceInfoCache for managing device feature data with configurable TTL, and a @requires_capability decorator for automatic validation. Additionally, the PR adds advanced control commands for demand response, air filter maintenance, vacation mode, and recirculation pump control, all with automatic capability checking.
Key Changes:
- Device capability framework with automatic validation and caching (30-minute default TTL)
- Seven new advanced MQTT control commands with automatic capability verification
- Enhanced CLI with new diagnostic and control options
- Comprehensive documentation including new device_control.rst guide and example scripts
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/nwp500/device_capabilities.py | Core capability checker using mapping-based validation |
| src/nwp500/device_info_cache.py | Async cache for device features with TTL expiration |
| src/nwp500/command_decorators.py | @requires_capability decorator with auto-request fallback |
| src/nwp500/mqtt_device_control.py | Seven new control commands with capability decorators |
| src/nwp500/mqtt_client.py | Integration of cache and auto-request infrastructure |
| src/nwp500/mqtt_subscriptions.py | Auto-caching of device features on receipt |
| src/nwp500/mqtt_connection.py | Type hint fix for payload parameter |
| src/nwp500/models.py | New fields for recirculation and DHW control precision |
| src/nwp500/enums.py | DHWControlTypeFlag enum for temperature control granularity |
| src/nwp500/exceptions.py | DeviceCapabilityError for unsupported features |
| src/nwp500/api_client.py | Type hint improvements and None value filtering |
| src/nwp500/init.py | Export new capability classes and decorators |
| src/nwp500/cli/main.py | Seven new CLI commands with early capability checking |
| src/nwp500/cli/commands.py | Handler functions for new control commands |
| examples/*.py | Four example scripts demonstrating new features |
| docs/python_api/device_control.rst | Complete control API reference (1188 lines) |
| docs/python_api/mqtt_client.rst | Updated to reference capability checking |
| docs/python_api/exceptions.rst | DeviceCapabilityError documentation |
| docs/protocol/mqtt_protocol.rst | Corrected command codes and added new commands |
…tures - Add 14 tests for DeviceCapabilityChecker - Add 25 tests for DeviceInfoCache async caching operations - Add 12 tests for requires_capability decorator - Total: 51 new tests with 100%, 97%, and 100% code coverage respectively Tests cover: - Capability detection and validation - Cache expiration and concurrent access - Decorator behavior with supported/unsupported features - Auto-request of device info when not cached - Proper exception handling and logging
Security fixes: - Remove sensitive device MAC logging from device_info_cache.py - Use TYPE_CHECKING import for DeviceInfoCache type hint in mqtt_subscriptions.py Type safety improvements: - Add TypedDict for CachedDeviceInfo and CacheInfoResult in device_info_cache.py - Fix type annotation for clean_params and clean_json_data in api_client.py - Remove unnecessary cast() usage in api_client.py Validation: - Add max_value=365 to vacation days validation in mqtt_device_control.py Code quality: - Refactor duplicate device info request handlers into shared helper function - Fix error message formatting in cli/__main__.py - Reorder MQTT protocol table for logical flow - Clarify temperature formula documentation in data_conversions.rst
Changed _ensure_device_info_cached to ensure_device_info_cached to indicate this is a public API method that can be called from CLI and other external code, not just an internal method for control commands. This addresses the review comment about accessing protected methods.
Added upper bound check to validate that vacation days are between 1-365. Previously only checked for days <= 0, but didn't enforce the 365-day maximum recommended in the docstring. This addresses the code review comment about enforcing the max_value constraint.
Changed error message from single long line to implicit string concatenation pattern used elsewhere in the file to stay within 80-character line limit. This addresses the review comment about consistency with other similar messages.
Addresses PR #52 review feedback: - ✓ Line 127: Using public ensure_device_info_cached() method (commit f0edcca) - ✓ Line 300: Using implicit string concatenation (commit 55e6650) - ✓ Line 199: Refactored with shared _request_device_info_common() helper - ✓ All linting checks pass - ✓ All type checks pass - ✓ All tests pass (210/210)
…requests, and fix parsing regressions - Consolidated device control methods and removed mirrored interfaces in NavienMqttClient. - Introduced MqttTopicBuilder and improved MqttDeviceController command dispatch. - Unified periodic request methods into a single start_periodic_requests API. - Fixed regressions in CLI, documentation, and 40+ examples to match the new API. - Enhanced DeviceFeature model with AvailabilityFlag and CapabilityFlag for accurate protocol parsing. - Improved NavienAPIClient logging and NavienAuthClient token refresh logic.
…ility - Added redact_mac and redact_serial helpers to mqtt_utils. - Masked MAC addresses and topics in core MQTT and decorated logs. - Refactored CLI status and info handlers to shared common logic. - Standardized CLI 'no action' error message onto a single line. - Reordered Device Info/Status commands in protocol docs for better flow. - Clarified temperature conversion math in documentation.
- Remove clear-text printing of access_token and refresh_token in examples - Replace with safe examples that print user names instead - Add comments noting tokens should not be printed in production - Fixes CodeQL alert: Clear-text logging of sensitive information
…tors - Remove f-string interpolation of mac address in warning log - Remove unused import of redact_mac - Remove unused mac variable assignment - Fixes CodeQL alert: Clear-text logging of sensitive data
- Remove f-string interpolation of mac address in TimeoutError log - Remove unused import of redact_mac - Fixes CodeQL alert: Clear-text logging of sensitive data
…ntrol - Remove f-string interpolation of mac address in info log - Remove unused import of redact_mac - Fixes CodeQL alert: Clear-text logging of sensitive data
- Fix type annotations in models._convert_enums_to_names for dict/list return type - Convert IntEnum device_type values to strings for MQTT topic builders - Add proper type casting in api_client._make_request for ClientSession - Create async wrapper for ensure_device_info_cached callback in mqtt_client - Use cast() for method return type in mqtt_client._delegate_subscription - Import DeviceFeature and EnergyUsageResponse in cli/commands - Fix energy request to use subscribe_energy_usage instead of subscribe_device - Break long import lines to comply with 80-character limit - All 209 tests passing, linting and type checking clean
- Create field_factory.py module with reusable field creation functions - Add temperature_field(), signal_strength_field(), energy_field(), power_field() - Refactor ~30 field definitions in DeviceStatus and DeviceFeature - Reduces models.py from 1290 to 1143 lines (147 lines saved) - Eliminates repetitive json_schema_extra definitions - Maintains backward compatibility - all 209 tests passing - Improves maintainability by centralizing metadata configuration NET IMPACT: - Added: field_factory.py (150 lines) - Removed: 147 lines from models.py - Net change: -3 LOC, but significantly improved readability
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 75 out of 76 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
tests/test_command_decorators.py:1
- Using lambda with noqa disables the linting rule that suggests using def instead. While lambda is convenient for tests, using a proper function definition improves readability and avoids the need for noqa comments. Consider:
def custom_check(f): return True
"""Tests for command decorators."""
…resses The security scanner detects sensitive variables being used in string interpolation before redaction. Store the redacted value in an intermediate variable before logging to prevent this detection.
| @staticmethod | ||
| def _convert_enums_to_names( | ||
| data: Any, visited: set[int | None] = None | ||
| data: Any, visited: set[int] | None = None |
There was a problem hiding this comment.
The docstring comment about 'Object IDs are never None' is incorrect. The parameter annotation shows visited can be None on initial call. Update docstring to clarify that None indicates uninitialized/first call.
| import csv | ||
| import json | ||
| import logging | ||
| from calendar import month_name |
There was a problem hiding this comment.
The month_name import is used without bounds checking in line 100. Consider adding validation to ensure month_data.month is in valid range (1-12) before indexing.
| print(" python -m nwp500.cli --configure-water-program") | ||
| print(" python -m nwp500.cli --configure-water-program --status") |
There was a problem hiding this comment.
The CLI command examples reference deprecated command-line syntax. According to the CLI refactor in __main__.py, commands now use subcommands (e.g., water-program). Update to reflect new syntax.
| print(" python -m nwp500.cli --configure-water-program") | |
| print(" python -m nwp500.cli --configure-water-program --status") | |
| print(" python -m nwp500 water-program") | |
| print(" python -m nwp500 water-program --status") |
examples/vacation_mode_example.py
Outdated
| print(" python -m nwp500.cli --set-vacation-days 7") | ||
| print(" python -m nwp500.cli --set-vacation-days 14 --status") | ||
| print(" python -m nwp500.cli --set-vacation-days 21 --status") |
There was a problem hiding this comment.
CLI command examples use old flag-based syntax. Update to new subcommand syntax: python -m nwp500.cli vacation 7
| print(" python -m nwp500.cli --set-vacation-days 7") | |
| print(" python -m nwp500.cli --set-vacation-days 14 --status") | |
| print(" python -m nwp500.cli --set-vacation-days 21 --status") | |
| print(" python -m nwp500.cli vacation 7") | |
| print(" python -m nwp500.cli vacation 14 --status") | |
| print(" python -m nwp500.cli vacation 21 --status") |
| print(" python -m nwp500.cli --set-recirculation-mode 1") | ||
| print(" python -m nwp500.cli --recirculation-hot-button") | ||
| print(" python -m nwp500.cli --set-recirculation-mode 2 --status") |
There was a problem hiding this comment.
CLI examples use deprecated flag-based syntax. Update to new subcommand syntax: python -m nwp500.cli recirc 1 and python -m nwp500.cli hot-button
| print(" python -m nwp500.cli --set-recirculation-mode 1") | |
| print(" python -m nwp500.cli --recirculation-hot-button") | |
| print(" python -m nwp500.cli --set-recirculation-mode 2 --status") | |
| print(" python -m nwp500.cli recirc 1") | |
| print(" python -m nwp500.cli hot-button") | |
| print(" python -m nwp500.cli recirc 2 --status") |
examples/demand_response_example.py
Outdated
| print(" python -m nwp500.cli --enable-demand-response") | ||
| print(" python -m nwp500.cli --disable-demand-response") | ||
| print(" python -m nwp500.cli --enable-demand-response --status") |
There was a problem hiding this comment.
CLI examples use old syntax. Update to new subcommand format: python -m nwp500.cli dr enable and python -m nwp500.cli dr disable
| print(" python -m nwp500.cli --enable-demand-response") | |
| print(" python -m nwp500.cli --disable-demand-response") | |
| print(" python -m nwp500.cli --enable-demand-response --status") | |
| print(" python -m nwp500.cli dr enable") | |
| print(" python -m nwp500.cli dr disable") | |
| print(" python -m nwp500.cli dr enable --status") |
examples/air_filter_reset_example.py
Outdated
| print(" python -m nwp500.cli --reset-air-filter") | ||
| print(" python -m nwp500.cli --reset-air-filter --status") |
There was a problem hiding this comment.
CLI examples use deprecated flag syntax. Update to new subcommand syntax: python -m nwp500.cli reset-filter
| print(" python -m nwp500.cli --reset-air-filter") | |
| print(" python -m nwp500.cli --reset-air-filter --status") | |
| print(" python -m nwp500.cli reset-filter") | |
| print(" python -m nwp500.cli reset-filter --status") |
src/nwp500/api_client.py
Outdated
| filtered_params: dict[str, Any] = {} | ||
| for k, v in params.items(): | ||
| if v is not None: | ||
| filtered_params[k] = v | ||
| clean_params = filtered_params | ||
| if json_data: | ||
| filtered_json: dict[str, Any] = {} | ||
| for k, v in json_data.items(): | ||
| if v is not None: | ||
| filtered_json[k] = v | ||
| clean_json_data = filtered_json |
There was a problem hiding this comment.
Use dictionary comprehension for more concise and Pythonic filtering: filtered_params = {k: v for k, v in params.items() if v is not None}
| filtered_params: dict[str, Any] = {} | |
| for k, v in params.items(): | |
| if v is not None: | |
| filtered_params[k] = v | |
| clean_params = filtered_params | |
| if json_data: | |
| filtered_json: dict[str, Any] = {} | |
| for k, v in json_data.items(): | |
| if v is not None: | |
| filtered_json[k] = v | |
| clean_json_data = filtered_json | |
| clean_params = {k: v for k, v in params.items() if v is not None} | |
| if json_data: | |
| clean_json_data = { | |
| k: v for k, v in json_data.items() if v is not None | |
| } |
- Fix models.py docstring: clarify None means uninitialized/first call - Improve output_formatters.py: add lower bound check for month validation (1-12) - Refactor api_client.py: use dictionary comprehensions instead of loops - Reorganize mqtt_client.py: redact MAC address earlier to prevent clear-text logging - Update example files: reflect new CLI subcommand syntax (vacation, recirc, dr, reset-filter, water-program)
- Update CHANGELOG.rst with comprehensive v7.1.0 release notes - Document all device capabilities and advanced control features - Document CLI documentation updates and examples - Document model refactoring and security improvements - Consolidate all 25+ commits since v7.0.1 - Update CLI documentation (docs/python_api/cli.rst) - Complete rewrite for subcommand-based interface - Add 18+ command reference documentation - Add 8+ practical usage examples - Add troubleshooting and best practices sections - Update README.rst - Replace old flag-based CLI documentation with subcommand examples - Add examples for all major commands - Update example scripts - power_control_example.py: Use new CLI subcommand syntax - set_mode_example.py: Use new CLI subcommand syntax - set_dhw_temperature_example.py: Use new CLI subcommand syntax - Update protocol documentation - firmware_tracking.rst: Update old CLI reference to new syntax All checks passing: linting, type checking, tests
Advanced MQTT Control Commands:
Enhancements:
Bug Fixes & Improvements: