Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@
Changelog
=========

Unreleased
==========

Fixed
-----
- **Anti-Legionella set-period State Preservation**: ``nwp-cli anti-legionella
set-period`` was calling ``enable_anti_legionella()`` in both the enabled and
disabled branches, silently re-enabling the feature when it was off. The
command now informs the user that the period can only be updated while the
feature is enabled and directs them to ``anti-legionella enable``.
- **Subscription State Lost After Failed Resubscription**: ``resubscribe_all()``
cleared ``_subscriptions`` and ``_message_handlers`` before the re-subscribe
loop. Topics that failed to resubscribe were permanently dropped from internal
state and could not be retried on the next reconnection. Failed topics are now
restored so they are retried automatically.
- **Unit System Detection Returns None on Timeout**: ``_detect_unit_system()``
declared return type ``UnitSystemType`` but returned ``None`` on
``TimeoutError``, violating the type contract. Now returns
``"us_customary"`` consistent with the warning message.
- **Once-Listener Becomes Permanent With Duplicate Callbacks**: ``emit()``
identified once-listeners via a ``set`` of ``(event, callback)`` tuples. If
the same callback was registered twice with ``once=True``, the set
deduplicated the tuple — after the first emit the second listener lost its
once-status and became permanent. Fixed by checking ``listener.once``
directly on the ``EventListener`` object.
- **Auth Session Leaked on Client Construction Failure**: In
``create_navien_clients()``, if ``NavienAPIClient`` or
``NavienMqttClient`` construction raised after a successful
``auth_client.__aenter__()``, the auth session and its underlying
``aiohttp`` session would leak. Client construction is now wrapped in a
``try/except`` that calls ``auth_client.__aexit__()`` on failure.
- **Hypothesis Tests Broke All Test Collection**: ``test_mqtt_hypothesis.py``
imported ``hypothesis`` at module level; when it was not installed, pytest
failed to collect every test in the suite. ``hypothesis`` is now mandated
as a ``[testing]`` extra dependency, restoring correct collection behaviour.

Changed
-------
- **Dependency: awsiotsdk >= 1.28.2**: Bumped minimum ``awsiotsdk`` version
from ``>=1.27.0`` to ``>=1.28.2`` to track the current patch release.
``awscrt`` 0.31.3 is pulled in transitively.

Version 7.4.8 (2026-02-17)
==========================

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ python_requires = >=3.13
# For more information, check out https://semver.org/.
install_requires =
aiohttp>=3.8.0
awsiotsdk>=1.27.0
awsiotsdk>=1.28.2
pydantic>=2.0.0


Expand Down
2 changes: 1 addition & 1 deletion src/nwp500/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _on_status(status: DeviceStatus) -> None:
_logger.warning(
"Timed out detecting unit system, defaulting to us_customary"
)
return None
return "us_customary"


def async_command(f: Any) -> Any:
Expand Down
11 changes: 6 additions & 5 deletions src/nwp500/cli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,15 @@ def _on_status(status: DeviceStatus) -> None:
# Get current enabled state
use = getattr(status, "anti_legionella_use", None)

# If enabled, keep it enabled; otherwise, enable it
# (period only, no disable-state for set operation)
if use:
await mqtt.control.enable_anti_legionella(device, period_days)
print(f"Anti-Legionella period set to {period_days} day(s)")
else:
await mqtt.control.enable_anti_legionella(device, period_days)

print(f"✓ Anti-Legionella period set to {period_days} day(s)")
print(
"Anti-Legionella is currently disabled. "
"Enable it first to set the period, or use "
"'anti-legionella enable' with the desired period."
)
except (RangeValidationError, ValidationError) as e:
_logger.error(f"Failed to set Anti-Legionella period: {e}")
except DeviceError as e:
Expand Down
4 changes: 2 additions & 2 deletions src/nwp500/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ async def emit(self, event: str, *args: Any, **kwargs: Any) -> int:

called_count += 1

# Check if this is a once listener using O(1) set lookup
if (event, listener.callback) in self._once_callbacks:
# Check if this is a once listener
if listener.once:
listeners_to_remove.append(listener)
self._once_callbacks.discard((event, listener.callback))

Expand Down
8 changes: 6 additions & 2 deletions src/nwp500/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ async def create_navien_clients(
raise

# Create API and MQTT clients that share the session
api_client = NavienAPIClient(auth_client=auth_client)
mqtt_client = NavienMqttClient(auth_client=auth_client)
try:
api_client = NavienAPIClient(auth_client=auth_client)
mqtt_client = NavienMqttClient(auth_client=auth_client)
except BaseException:
await auth_client.__aexit__(None, None, None)
raise

return auth_client, api_client, mqtt_client
13 changes: 12 additions & 1 deletion src/nwp500/mqtt/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,19 @@ async def resubscribe_all(self) -> None:
break # Exit handler loop, move to next topic

if failed_subscriptions:
# Restore failed subscriptions to internal state so they can be
# retried on the next reconnection cycle.
qos_map = dict(subscriptions_to_restore)
for topic in failed_subscriptions:
self._subscriptions[topic] = qos_map.get(
topic, mqtt.QoS.AT_LEAST_ONCE
)
self._message_handlers[topic] = handlers_to_restore.get(
topic, []
)
_logger.warning(
f"Failed to restore {len(failed_subscriptions)} subscription(s)"
f"Failed to restore {len(failed_subscriptions)} "
"subscription(s); will retry on next reconnection"
)
else:
_logger.info("All subscriptions re-established successfully")
Expand Down
Loading