Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
22c5e95
feat: enhance diagnostics, docs, and CLI tests
eman Dec 17, 2025
f856047
fix: resolve linting errors
eman Dec 17, 2025
90d9b15
fix: make lint script respect active python environment
eman Dec 17, 2025
ef09bcf
test: fix datetime.UTC attribute error in command queue tests
eman Dec 17, 2025
6a03f5e
test: fix docstring check to be more robust
eman Dec 17, 2025
4c18aa4
fix: add future annotations to diagnostics and utils for py39 compat
eman Dec 17, 2025
8767ece
fix: resolve remaining lint/type errors (UP045, whitepace)
eman Dec 17, 2025
d69ca26
fix: add future annotations to remaining modules for py39 compat
eman Dec 17, 2025
795f44c
style: fix formatting in mqtt_periodic.py
eman Dec 17, 2025
46b49a8
fix: Replace asyncio.Queue with deque for Python 3.9 compatibility
eman Dec 17, 2025
678f4a5
fix: Remove unused asyncio import
eman Dec 17, 2025
c9d8d19
refactor: Condense verbose test comment
eman Dec 17, 2025
e4b1400
fix: Replace deprecated datetime.utcnow() with datetime.now(timezone.…
eman Dec 17, 2025
319892d
fix: Fix line length issues for linting
eman Dec 17, 2025
24d75a3
feat: Drop Python 3.9, require Python 3.10+
eman Dec 18, 2025
93883da
refactor: Apply Python 3.10+ optimizations
eman Dec 18, 2025
ae43327
fix: Clean up imports and fix linting issues
eman Dec 18, 2025
8b6a632
fix: Correct Callable type annotation syntax
eman Dec 18, 2025
973b930
style: Auto-format models.py with ruff
eman Dec 18, 2025
7e35665
fix: Correct invalid None | None type annotation
eman Dec 18, 2025
a722dc7
feat: Drop Python 3.10-3.11, require Python 3.12+
eman Dec 18, 2025
a459304
fix: Consolidate changelog into single version 6.2.0
eman Dec 18, 2025
9f04ab0
feat: Drop Python 3.12, require Python 3.13+
eman Dec 18, 2025
28e5931
ci: Add Python 3.14 to test matrix
eman Dec 18, 2025
617997b
fix: Address PR review comments
eman Dec 18, 2025
4113e01
refactor: Use Python 3.11 Self type for context managers
eman Dec 18, 2025
3835519
docs: Update Python version requirement to 3.13+
eman Dec 18, 2025
55bf853
chore: Update awsiotsdk requirement to >=1.27.0
eman Dec 18, 2025
485cf6e
update the version to 7.0.0
eman Dec 18, 2025
be8e8ea
remove unintentional file
eman Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.13', '3.14']

steps:
- uses: actions/checkout@v4
Expand Down
32 changes: 30 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,31 @@
Changelog
=========

Version 6.2.0 (2025-12-17)
Version 7.0.0 (2025-12-17)
==========================

**BREAKING CHANGES**: Enumerations refactored for type safety and consistency
**BREAKING CHANGES**:
- Minimum Python version raised to 3.13
- Enumerations refactored for type safety and consistency

Removed
-------
- **Python 3.9-3.12 Support**: Minimum Python version is now 3.13

Home Assistant has deprecated Python 3.12 support, making Python 3.13 the de facto minimum for this ecosystem.

Python 3.13 features and improvements:

- **Experimental free-threaded mode** (PEP 703): Optional GIL removal for true parallelism
- **JIT compiler** (PEP 744): Just-in-time compilation for performance improvements
- **Better error messages**: Enhanced suggestions for NameError, AttributeError, and import errors
- **Type system enhancements**: TypeVars with defaults (PEP 696), @deprecated decorator (PEP 702), ReadOnly TypedDict (PEP 705)
- **Performance**: ~5-10% faster overall, optimized dictionary/set operations, better function calls
- PEP 695: New type parameter syntax for generics
- PEP 701: f-string improvements
- Built-in ``datetime.UTC`` constant

If you need Python 3.12 support, use version 6.1.x of this library.

- **CommandCode moved**: Import from ``nwp500.enums`` instead of ``nwp500.constants``

Expand All @@ -22,6 +43,13 @@ Version 6.2.0 (2025-12-17)
Added
-----

- **Python 3.12+ Optimizations**: Leverage latest Python features

- PEP 695: New type parameter syntax (``def func[T](...)`` instead of ``TypeVar``)
- Use ``datetime.UTC`` constant instead of ``datetime.timezone.utc``
- Native union syntax (``X | Y`` instead of ``Union[X, Y]``)
- Cleaner generic type annotations throughout codebase

- **Enumerations Module (``src/nwp500/enums.py``)**: Comprehensive type-safe enums for device control and status

- Status value enums: ``OnOffFlag``, ``Operation``, ``DhwOperationSetting``, ``CurrentOperationMode``, ``HeatSource``, ``DREvent``, ``WaterLevel``, ``FilterChange``, ``RecirculationMode``
Expand Down
6 changes: 2 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,10 @@ The library includes type-safe data models with automatic unit conversions:
Requirements
============

* Python 3.9+
* Python 3.13+
* aiohttp >= 3.8.0
* websockets >= 10.0
* cryptography >= 3.4.0
* pydantic >= 2.0.0
* awsiotsdk >= 1.21.0
* awsiotsdk >= 1.27.0

License
=======
Expand Down
4 changes: 2 additions & 2 deletions docs/development/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ providing:
- Automatic reconnection with exponential backoff
- Command queuing for reliable communication
- Historical energy usage data (EMS API)
- Modern Python 3.9+ codebase with native type hints
- Modern Python 3.13+ codebase with native type hints

Current Status
--------------
Expand All @@ -38,7 +38,7 @@ The library is feature-complete with:
- Comprehensive documentation
- Working examples for all features
- Unit tests with good coverage
- Python 3.9+ with modern type hints
- Python 3.13+ with modern type hints

Implementation Milestones
-------------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Installation
Requirements
============

* Python 3.9 or higher
* Python 3.13 or higher
* pip (Python package installer)
* Navien Smart Control account

Expand Down Expand Up @@ -51,7 +51,7 @@ Core Dependencies
The library requires:

* ``aiohttp>=3.8.0`` - Async HTTP client for REST API
* ``awsiotsdk>=1.20.0`` - AWS IoT SDK for MQTT
* ``awsiotsdk>=1.27.0`` - AWS IoT SDK for MQTT
* ``pydantic>=2.0.0`` - Data validation and models

Optional Dependencies
Expand Down Expand Up @@ -128,7 +128,7 @@ The MQTT client requires the AWS IoT SDK:

.. code-block:: bash

pip install awsiotsdk>=1.20.0
pip install awsiotsdk>=1.27.0

Upgrading
=========
Expand Down
2 changes: 1 addition & 1 deletion docs/python_api/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Complete real-time device status with 100+ fields.
# Water usage detection
if status.dhw_use:
print("Water usage detected (short-term)")
if status.dhw_useSustained:
if status.dhw_use_sustained:
print("Water usage detected (sustained)")

# Errors
Expand Down
16 changes: 10 additions & 6 deletions docs/python_api/mqtt_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ subscribe_device_status()
print(f"Target: {status.dhw_temperature_setting}°F")
print(f"Mode: {status.dhw_operation_setting.name}")
print(f"Power: {status.current_inst_power}W")
print(f"Energy: {status.available_energy_capacity}%")
print(f"Energy: {status.dhw_charge_per}%")

# Check if actively heating
if status.operation_busy:
Expand Down Expand Up @@ -566,11 +566,15 @@ subscribe_energy_usage()
print(f"Electric: {energy.total.heat_element_percentage:.1f}%")

print("\nDaily Breakdown:")
for day_data in energy.data:
print(f" Date: Day {len(energy.data)}")
print(f" Total: {day_data.total_usage} Wh")
print(f" HP: {day_data.hpUsage} Wh ({day_data.hpTime}h)")
print(f" HE: {day_data.heUsage} Wh ({day_data.heTime}h)")
for monthly_data in energy.usage:
print(f" Month: {monthly_data.year}-{monthly_data.month}")
for day_data in monthly_data.data:
# Skip empty days (all zeros)
if day_data.total_usage > 0:
print(f" Day {monthly_data.data.index(day_data) + 1}:")
print(f" Total: {day_data.total_usage} Wh")
print(f" HP: {day_data.heat_pump_usage} Wh ({day_data.heat_pump_time}h)")
print(f" HE: {day_data.heat_element_usage} Wh ({day_data.heat_element_time}h)")

await mqtt.subscribe_energy_usage(device, on_energy)
await mqtt.request_energy_usage(device, year=2024, months=[10])
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ in just a few minutes.
Prerequisites
=============

* Python 3.9 or higher
* Python 3.13 or higher
* Navien Smart Control account (via Navilink mobile app)
* At least one Navien NWP500 device registered to your account
* Valid email and password for your Navien account
Expand Down
9 changes: 4 additions & 5 deletions examples/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
from __future__ import annotations

import re
from typing import Optional


def mask_mac(mac: Optional[str]) -> str:
def mask_mac(mac: str | None) -> str:
"""Always return fully redacted MAC address label, never expose partial values."""
return "[REDACTED_MAC]"


def mask_mac_in_topic(topic: str, mac_addr: Optional[str] = None) -> str:
def mask_mac_in_topic(topic: str, mac_addr: str | None = None) -> str:
"""Return topic with any MAC-like substrings replaced.

Also ensures a direct literal match of mac_addr is redacted.
Expand All @@ -33,7 +32,7 @@ def mask_mac_in_topic(topic: str, mac_addr: Optional[str] = None) -> str:
__all__ = ["mask_mac", "mask_mac_in_topic"]


def mask_any(value: Optional[str]) -> str:
def mask_any(value: str | None) -> str:
"""Generic redaction for strings considered sensitive in examples.

Always returns a short redaction tag; keep implementation simple so examples
Expand All @@ -51,7 +50,7 @@ def mask_any(value: Optional[str]) -> str:
return "[REDACTED]"


def mask_location(city: Optional[str], state: Optional[str]) -> str:
def mask_location(city: str | None, state: str | None) -> str:
"""Redact location fields for examples.

Returns a single redaction tag if either city or state are present.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ version_scheme = "no-guess-dev"
[tool.ruff]
# Ruff configuration for code formatting and linting
line-length = 80
target-version = "py39"
target-version = "py313"

# Exclude directories
exclude = [
Expand Down Expand Up @@ -99,7 +99,7 @@ line-ending = "auto"
strict = true

# Python version target
python_version = "3.9"
python_version = "3.13"

# Module discovery
files = ["src/nwp500", "tests"]
Expand Down
174 changes: 174 additions & 0 deletions scripts/diagnose_mqtt_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
MQTT Connection Diagnostic Tool for Navien Smart Control.

This script connects to the MQTT broker and monitors connection stability for a
specified duration. It outputs detailed diagnostics including:
- Connection drops and error reasons
- Reconnection attempts
- Session duration statistics
- Message throughput

Usage:
python scripts/diagnose_mqtt_connection.py [--duration SECONDS] [--verbose]
"""

import argparse
import asyncio
import contextlib
import logging
import os
import signal
import sys
from datetime import datetime

# Add src to path to allow running from project root
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))

try:
from nwp500 import NavienAuthClient, NavienMqttClient
from nwp500.mqtt_utils import MqttConnectionConfig
except ImportError:
print(
"Error: Could not import nwp500 library. "
"Run from project root with installed dependencies."
)
sys.exit(1)

# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("mqtt_diagnostics")


async def main():
parser = argparse.ArgumentParser(description="MQTT Connection Diagnostics")
parser.add_argument(
"--duration",
type=int,
default=60,
help="Duration to run monitoring in seconds (0 for indefinite)",
)
parser.add_argument(
"--verbose", action="store_true", help="Enable verbose logging"
)
parser.add_argument(
"--email", help="Navien account email (or use NAVIEN_EMAIL env var)"
)
parser.add_argument(
"--password",
help="Navien account password (or use NAVIEN_PASSWORD env var)",
)

args = parser.parse_args()

# Get credentials
email = args.email or os.getenv("NAVIEN_EMAIL")
password = args.password or os.getenv("NAVIEN_PASSWORD")

if not email or not password:
print("Error: Credentials required.")
print("Set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables")
print("OR provide --email and --password arguments.")
sys.exit(1)

if args.verbose:
logging.getLogger("nwp500").setLevel(logging.DEBUG)
logging.getLogger("awscrt").setLevel(logging.DEBUG)

print(f"Starting MQTT diagnostics for {email}")
print(f"Monitoring duration: {args.duration} seconds")
print("Press Ctrl+C to stop early and generate report")
print("-" * 60)

try:
async with NavienAuthClient(email, password) as auth_client:
# Configure connection for investigation
config = MqttConnectionConfig(
auto_reconnect=True,
max_reconnect_attempts=5,
enable_command_queue=True,
)

mqtt_client = NavienMqttClient(auth_client, config=config)

# Setup signal handler for graceful shutdown
stop_event = asyncio.Event()

def signal_handler():
print("\nStopping diagnostics...")
stop_event.set()

loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, signal_handler)

# Connect
logger.info("Connecting to MQTT broker...")
await mqtt_client.connect()
logger.info("Connected!")

# Start monitoring loop
start_time = datetime.now()
conn_start = start_time

while not stop_event.is_set():
# Check duration
if (
args.duration > 0
and (datetime.now() - start_time).total_seconds()
> args.duration
):
logger.info("Duration reached.")
break

# Print periodic status
if (datetime.now() - conn_start).total_seconds() >= 10:
metrics = mqtt_client.diagnostics.get_metrics()
uptime = metrics.current_session_uptime_seconds
drops = metrics.total_connection_drops
reconnects = metrics.connection_recovered

status = (
"Connected"
if mqtt_client.is_connected
else "Disconnected"
)
print(
f"Status: {status} | "
f"Uptime: {uptime:.1f}s | "
f"Drops: {drops} | "
f"Reconnects: {reconnects}"
)
conn_start = datetime.now()

await asyncio.sleep(1)

# Final Summary
print("\n" + "=" * 60)
print("DIAGNOSTIC REPORT")
print("=" * 60)
mqtt_client.diagnostics.print_summary()

# Export JSON
json_report = mqtt_client.diagnostics.export_json()
report_file = (
f"mqtt_diag_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
)
with open(report_file, "w") as f:
f.write(json_report)
print(f"\nDetailed JSON report saved to: {report_file}")

# Disconnect
await mqtt_client.disconnect()

except Exception as e:
logger.error(f"Diagnostic error: {e}", exc_info=True)
sys.exit(1)


if __name__ == "__main__":
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(main())
Loading