feat: enhance diagnostics, docs, and CLI tests#50
Conversation
- Integrate MqttDiagnosticsCollector into NavienMqttClient - Add diagnose_mqtt_connection.py script - Fix documentation inconsistencies (camelCase vs snake_case, units) - Add CLI unit tests (basic coverage and command mocking) - Fix deprecation warnings in utils and tests
In Python 3.9, asyncio.Queue() calls get_event_loop() during __init__, which fails when there's no running event loop. Since the queue is accessed synchronously (using put_nowait/get_nowait), we can use collections.deque instead, which doesn't require an event loop. This fixes CI failures on Python 3.9 while maintaining the same functionality - FIFO queue with automatic overflow dropping when full. Fixes: 6 test failures in test_mqtt_client_init.py
There was a problem hiding this comment.
Pull request overview
This PR enhances the nwp500 project by integrating diagnostics collection, fixing documentation inconsistencies, adding CLI unit tests, and addressing deprecation warnings. The changes improve observability of MQTT connections and expand test coverage while modernizing deprecated datetime usage.
Key changes:
- Integrated MqttDiagnosticsCollector into NavienMqttClient for connection monitoring
- Added diagnostic script for troubleshooting MQTT connections
- Fixed documentation examples to match actual API field names
- Replaced deprecated datetime.utcnow() with timezone-aware alternatives
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/nwp500/mqtt_client.py | Integrated diagnostics collector and added connection event recording |
| src/nwp500/mqtt_diagnostics.py | Updated type hints to use PEP 604 union syntax |
| src/nwp500/mqtt_command_queue.py | Replaced asyncio.Queue with deque for simpler synchronous queue management |
| src/nwp500/mqtt_*.py | Standardized type hints across MQTT modules using modern union syntax |
| src/nwp500/utils.py | Replaced asyncio.iscoroutinefunction with inspect.iscoroutinefunction |
| tests/test_mqtt_client_init.py | Fixed test to avoid bound method docstring issues |
| tests/test_command_queue.py | Replaced deprecated datetime.utcnow() with timezone-aware datetime.now(timezone.utc) |
| tests/test_cli_*.py | Added comprehensive unit tests for CLI command handlers |
| scripts/diagnose_mqtt_connection.py | Added diagnostic tool for monitoring MQTT connection stability |
| scripts/lint.py | Updated to use sys.executable instead of hardcoded python3 |
| scripts/extract_changelog.py | Removed redundant file mode and added noqa comment for long line |
| docs/python_api/*.rst | Fixed field name inconsistencies in documentation examples |
Comments suppressed due to low confidence (3)
src/nwp500/mqtt_diagnostics.py:181
- Use
datetime.now(timezone.utc)instead of deprecateddatetime.utcnow(). The current approach creates a timezone-naive datetime and manually appends 'Z', when timezone-aware datetimes handle this properly.
now = datetime.utcnow().isoformat() + "Z"
src/nwp500/mqtt_diagnostics.py:271
- Use
datetime.now(timezone.utc)instead of deprecateddatetime.utcnow(). Same issue as Comment 2 - creates timezone-naive datetime and manually appends 'Z'.
now = datetime.utcnow().isoformat() + "Z"
src/nwp500/mqtt_diagnostics.py:372
- Use
datetime.now(timezone.utc)instead of deprecateddatetime.utcnow(). Consistent with previous comments - this deprecated usage should be replaced for consistency with test_command_queue.py changes.
"timestamp": datetime.utcnow().isoformat() + "Z",
…utc) Using datetime.now(timezone.utc) instead of the deprecated utcnow() to create timezone-aware datetime objects and avoid subtle bugs. Consistent with fixes in test_command_queue.py.
- Use native union syntax (X | Y instead of Union[X, Y]) - Use native optional syntax (X | None instead of Optional[X]) - Import Callable from collections.abc instead of typing - Add strict=True to zip() calls for safety These changes leverage Python 3.10+ features now that we've dropped Python 3.9 support.
- Remove unused Optional and Union imports - Consolidate collections.abc imports on single line - Fix import ordering per isort rules - Fix line length on zip() call with strict parameter
The return type in Callable must be separate from args. Changed Callable[[Any, Any | None, None]] to Callable[[Any, Any | None], None]
Changed Callable[..., None | None] to Callable[..., None] | None The return type was incorrectly specified as None | None which is invalid.
All changes in this PR are part of version 6.2.0. Removed artificial version 7.0.0 and 8.0.0 entries and consolidated all breaking changes and features into the single upcoming release.
Test on both Python 3.13 and 3.14 to ensure forward compatibility.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
src/nwp500/models.py:1
- The type annotation
list[int | None]should belist[int] | Nonebecausemonthis an optional list of integers, not a list that can contain None values. The current annotation incorrectly suggests the list can contain None as an element.
"""Data models for Navien NWP500 water heater communication.
| @staticmethod | ||
| def _convert_enums_to_names( | ||
| data: Any, visited: Optional[set[int]] = None | ||
| data: Any, visited: set[int | None] = None |
There was a problem hiding this comment.
The type annotation set[int | None] should be set[int] | None because visited is an optional set of integers, not a set that can contain None values. The current annotation incorrectly suggests the set can contain None as an element.
| data: Any, visited: set[int | None] = None | |
| data: Any, visited: set[int] | None = None |
| context: Optional[dict[str, Any]] = None, | ||
| strict: bool | None = None, | ||
| from_attributes: bool | None = None, | ||
| context: dict[str, Any | None] = None, |
There was a problem hiding this comment.
The type annotation dict[str, Any | None] should be dict[str, Any] | None because context is an optional dictionary, not a dictionary with optional values. The current annotation incorrectly suggests the dictionary values can be None rather than the entire parameter being optional.
| context: dict[str, Any | None] = None, | |
| context: dict[str, Any] | None = None, |
| self, | ||
| topic: str, | ||
| payload: Union[str, dict[str, Any], Any], | ||
| payload: str | dict[str, Any, Any], |
There was a problem hiding this comment.
The type annotation dict[str, Any, Any] is invalid syntax. Dictionary type hints should be dict[KeyType, ValueType] with only two type parameters. This should be dict[str, Any] instead.
| payload: str | dict[str, Any, Any], | |
| payload: str | bytes | dict[str, Any], |
| error_code: Optional[str] = None, | ||
| details: Optional[dict[str, Any]] = None, | ||
| error_code: str | None = None, | ||
| details: dict[str, Any | None] = None, |
There was a problem hiding this comment.
The type annotation dict[str, Any | None] should be dict[str, Any] | None because details is an optional dictionary parameter, not a dictionary with optional values. The current annotation incorrectly suggests the dictionary values can be None rather than the entire parameter being optional.
| details: dict[str, Any | None] = None, | |
| details: dict[str, Any] | None = None, |
| status_code: Optional[int] = None, | ||
| response: Optional[dict[str, Any]] = None, | ||
| status_code: int | None = None, | ||
| response: dict[str, Any | None] = None, |
There was a problem hiding this comment.
The type annotation dict[str, Any | None] should be dict[str, Any] | None because response is an optional dictionary parameter, not a dictionary with optional values. The current annotation incorrectly suggests the dictionary values can be None rather than the entire parameter being optional.
| code: Optional[int] = None, | ||
| response: Optional[dict[str, Any]] = None, | ||
| code: int | None = None, | ||
| response: dict[str, Any | None] = None, |
There was a problem hiding this comment.
The type annotation dict[str, Any | None] should be dict[str, Any] | None because response is an optional dictionary parameter, not a dictionary with optional values. The current annotation incorrectly suggests the dictionary values can be None rather than the entire parameter being optional.
|
|
||
| def off( | ||
| self, event: str, callback: Optional[Callable[..., Any]] = None | ||
| self, event: str, callback: Callable[..., Any | None] = None |
There was a problem hiding this comment.
The type annotation Callable[..., Any | None] should be Callable[..., Any] | None because callback is an optional callable parameter, not a callable that returns an optional value. The current annotation incorrectly suggests the callable returns an optional value rather than the entire parameter being optional.
| self, event: str, callback: Callable[..., Any | None] = None | |
| self, event: str, callback: Callable[..., Any] | None = None |
| json_data: dict[str, Any | None] = None, | ||
| params: dict[str, Any | None] = None, |
There was a problem hiding this comment.
The type annotations dict[str, Any | None] should be dict[str, Any] | None because these are optional dictionary parameters, not dictionaries with optional values. The current annotations incorrectly suggest the dictionary values can be None rather than the entire parameters being optional.
| json_data: dict[str, Any | None] = None, | |
| params: dict[str, Any | None] = None, | |
| json_data: dict[str, Any] | None = None, | |
| params: dict[str, Any] | None = None, |
- Replace deprecated datetime.utcnow() with datetime.now(UTC) in mqtt_diagnostics.py (3 instances) - Fix type annotation: month: list[int] | None (not list[int | None]) The field is an optional list of integers, not a list that can contain None values
Replace quoted forward references with Self type (PEP 673): - NavienAPIClient.__aenter__ now returns Self instead of 'NavienAPIClient' - NavienAuthClient.__aenter__ now returns Self instead of 'NavienAuthClient' Benefits: - Cleaner type annotations - Better type inference - More maintainable (no need to update quoted class names)
Updated documentation to reflect Python 3.13+ requirement: - docs/installation.rst: Prerequisites section - docs/quickstart.rst: Prerequisites section - docs/development/history.rst: Current status references Historical references to Python 3.9 migration remain unchanged as they document past milestones.
- Updated from 1.26.0 to latest version 1.27.0 - Made version consistent across all documentation - Removed incorrect dependencies from README (websockets, cryptography) These are transitive dependencies, not direct requirements All documentation now shows consistent dependency versions.
Overview
This PR enhances the nwp500 library with diagnostics capabilities, modernizes the codebase to Python 3.13+, and fixes various issues discovered during development.
Breaking Changes
Major Changes
1. MQTT Diagnostics & Monitoring
scripts/diagnose_mqtt_connection.py) for troubleshooting2. Python Modernization (3.13+ Features)
X | Yinstead ofUnion[X, Y])Selftype for context manager return typesdef func[T](...))datetime.UTCconstant, JIT compiler benefits3. CLI Enhancements
4. Bug Fixes
datetime.utcnow()→datetime.now(UTC)(4 instances)month: list[int] | None(waslist[int | None])Technical Improvements
Type Safety
Union[X, Y]withX | YsyntaxOptional[X]withX | NonesyntaxSelftype for methods returningselfCode Quality
strict=Trueto zip() calls for safetyTesting
Files Changed
Core Library:
src/nwp500/mqtt_client.py- Integrated diagnostics collectorsrc/nwp500/mqtt_diagnostics.py- Modernized datetime usagesrc/nwp500/mqtt_command_queue.py- Updated type hintssrc/nwp500/api_client.py- Used Self typesrc/nwp500/auth.py- Used Self typesrc/nwp500/models.py- Fixed type annotationsrc/nwp500/utils.py- PEP 695 genericsTests:
tests/test_cli_*.py- New CLI unit teststests/test_command_queue.py- Fixed datetime usagetests/test_mqtt_client_init.py- Fixed docstring issueDocumentation:
docs/python_api/*.rst- Fixed field name examplesREADME.rst- Updated Python version requirementCHANGELOG.rst- Consolidated all changes into v6.2.0Configuration:
setup.cfg- Python 3.13+ requirementpyproject.toml- Updated target version to py313.github/workflows/ci.yml- Test on Python 3.13 and 3.14Verification
✅ All linting checks pass (ruff)
✅ All formatting checks pass
✅ All tests pass on Python 3.13 and 3.14
✅ Type checking passes (mypy)
✅ Security scanning passes (CodeQL)
✅ Build distribution succeeds
Migration Guide
For users upgrading from v6.1.x:
For users who need Python 3.9-3.12: