nwp500-python 1.2.0__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/copilot-instructions.md +5 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/CHANGELOG.rst +10 -1
- {nwp500_python-1.2.0/src/nwp500_python.egg-info → nwp500_python-1.2.2}/PKG-INFO +1 -1
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/MQTT_CLIENT.rst +3 -2
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/anti_legionella_example.py +32 -3
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/mqtt_client.py +38 -52
- {nwp500_python-1.2.0 → nwp500_python-1.2.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.coveragerc +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/workflows/ci.yml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/workflows/release.yml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.gitignore +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.pre-commit-config.yaml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.readthedocs.yml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/AUTHORS.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/CONTRIBUTING.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/LICENSE.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/Makefile +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/README.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/RELEASE.md +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/API_CLIENT.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/API_REFERENCE.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTHENTICATION.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTO_RECOVERY.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTO_RECOVERY_QUICK.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/COMMAND_QUEUE.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVELOPMENT.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVICE_FEATURE_FIELDS.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVICE_STATUS_FIELDS.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/ENERGY_MONITORING.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/ERROR_CODES.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/EVENT_EMITTER.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/FIRMWARE_TRACKING.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/MQTT_MESSAGES.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/Makefile +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/_static/.gitignore +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/authors.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/changelog.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/conf.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/contributing.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/index.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/license.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/openapi.yaml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/readme.rst +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/requirements.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/.ruff.toml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/README.md +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/api_client_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/auth_constructor_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/authenticate.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/auto_recovery_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/combined_callbacks.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/command_queue_demo.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_feature_callback.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_status_callback.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_status_callback_debug.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/energy_usage_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/event_emitter_demo.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/improved_auth_pattern.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/mask.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/mqtt_client_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/periodic_device_info.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/periodic_requests.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/power_control_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/reconnection_demo.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/reservation_schedule_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/set_dhw_temperature_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/set_mode_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_auto_recovery.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_periodic_info.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_periodic_status.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_api_client.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_mqtt_connection.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_mqtt_messaging.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_periodic_minimal.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/tou_schedule_example.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/pyproject.toml +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/format.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/lint.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/setup-dev.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/setup.cfg +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/setup.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/__init__.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/api_client.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/auth.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/cli.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/config.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/constants.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/events.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/models.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/conftest.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_api_helpers.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_command_queue.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_events.py +0 -0
- {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tox.ini +0 -0
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
- **Install dependencies**: `pip install -e .` (development mode)
|
|
13
13
|
- **Run tests**: `pytest` (unit tests in `tests/`)
|
|
14
14
|
- **Lint/format**: `ruff format --check src/ tests/ examples/` (use `ruff format ...` to auto-format)
|
|
15
|
+
- **CI-compatible linting**: `make ci-lint` (run before finalizing changes to ensure CI will pass)
|
|
16
|
+
- **CI-compatible formatting**: `make ci-format` (auto-fix formatting issues)
|
|
15
17
|
- **Build docs**: `tox -e docs` (Sphinx docs in `docs/`)
|
|
16
18
|
- **Preview docs**: `python3 -m http.server --directory docs/_build/html`
|
|
17
19
|
|
|
20
|
+
### Before Committing Changes
|
|
21
|
+
Always run `make ci-lint` before finalizing changes to ensure your code will pass CI checks. This runs the exact same linting configuration as the CI pipeline, preventing "passes locally but fails in CI" issues.
|
|
22
|
+
|
|
18
23
|
## Patterns & Conventions
|
|
19
24
|
- **Async context managers** for authentication: `async with NavienAuthClient(email, password) as auth_client:`
|
|
20
25
|
- **Environment variables** for credentials: `NAVIEN_EMAIL`, `NAVIEN_PASSWORD`
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 1.2.2 (2025-10-17)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Fixed
|
|
9
|
+
-----
|
|
10
|
+
|
|
11
|
+
- Release version 1.2.2
|
|
12
|
+
|
|
5
13
|
Version 0.2 (Unreleased)
|
|
6
14
|
========================
|
|
7
15
|
|
|
@@ -19,9 +27,10 @@ Added
|
|
|
19
27
|
- Eliminates "passes locally but fails in CI" issues
|
|
20
28
|
- Cross-platform support (Linux, macOS, Windows, containers)
|
|
21
29
|
|
|
22
|
-
- All MQTT operations (connect, disconnect, subscribe, unsubscribe, publish) use ``asyncio.
|
|
30
|
+
- All MQTT operations (connect, disconnect, subscribe, unsubscribe, publish) use ``asyncio.wrap_future()`` to convert AWS SDK Futures to asyncio Futures
|
|
23
31
|
- Eliminates "blocking I/O detected" warnings in Home Assistant and other async applications
|
|
24
32
|
- Fully compatible with async event loops without blocking other operations
|
|
33
|
+
- More efficient than executor-based approaches (no thread pool usage)
|
|
25
34
|
- No API changes required - existing code works without modification
|
|
26
35
|
- Maintains full performance and reliability of the underlying AWS IoT SDK
|
|
27
36
|
- Safe for use in Home Assistant custom integrations and other async applications
|
|
@@ -1015,8 +1015,9 @@ async applications.
|
|
|
1015
1015
|
|
|
1016
1016
|
**Implementation Details:**
|
|
1017
1017
|
|
|
1018
|
-
-
|
|
1019
|
-
- Connection, disconnection, subscription, and publishing operations are non-blocking
|
|
1018
|
+
- AWS IoT SDK operations return ``concurrent.futures.Future`` objects that are converted to asyncio Futures using ``asyncio.wrap_future()``
|
|
1019
|
+
- Connection, disconnection, subscription, and publishing operations are fully non-blocking
|
|
1020
|
+
- No thread pool resources are used for MQTT operations (more efficient than executor-based approaches)
|
|
1020
1021
|
- The client maintains full compatibility with the existing API
|
|
1021
1022
|
- No additional configuration required for non-blocking behavior
|
|
1022
1023
|
|
|
@@ -17,6 +17,11 @@ import sys
|
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
19
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
20
|
+
from nwp500.constants import (
|
|
21
|
+
CMD_ANTI_LEGIONELLA_DISABLE,
|
|
22
|
+
CMD_ANTI_LEGIONELLA_ENABLE,
|
|
23
|
+
CMD_STATUS_REQUEST,
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def display_anti_legionella_status(status: dict[str, Any], label: str = "") -> None:
|
|
@@ -67,15 +72,35 @@ async def main() -> None:
|
|
|
67
72
|
latest_status = {}
|
|
68
73
|
status_received = asyncio.Event()
|
|
69
74
|
|
|
75
|
+
# Expected command codes for each step
|
|
76
|
+
expected_command = None
|
|
77
|
+
|
|
70
78
|
def on_status(topic: str, message: dict[str, Any]) -> None:
|
|
71
79
|
nonlocal latest_status
|
|
72
80
|
# Debug: print what we received
|
|
73
81
|
print(f"[DEBUG] Received message on topic: {topic}")
|
|
82
|
+
|
|
83
|
+
# Skip command echoes (messages on /ctrl topic)
|
|
84
|
+
if topic.endswith("/ctrl"):
|
|
85
|
+
print("[DEBUG] Skipping command echo")
|
|
86
|
+
return
|
|
87
|
+
|
|
74
88
|
status = message.get("response", {}).get("status", {})
|
|
89
|
+
command = status.get("command")
|
|
90
|
+
|
|
91
|
+
# Only capture status if it has Anti-Legionella data
|
|
75
92
|
if status.get("antiLegionellaPeriod") is not None:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
# If we're expecting a specific command, only accept that
|
|
94
|
+
if expected_command is None or command == expected_command:
|
|
95
|
+
latest_status = status
|
|
96
|
+
status_received.set()
|
|
97
|
+
print(
|
|
98
|
+
f"[DEBUG] Anti-Legionella status captured (command={command})"
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
print(
|
|
102
|
+
f"[DEBUG] Ignoring status from different command (got {command}, expected {expected_command})"
|
|
103
|
+
)
|
|
79
104
|
else:
|
|
80
105
|
print("[DEBUG] Message doesn't contain antiLegionellaPeriod")
|
|
81
106
|
|
|
@@ -94,6 +119,7 @@ async def main() -> None:
|
|
|
94
119
|
print("STEP 1: Getting initial Anti-Legionella status...")
|
|
95
120
|
print("=" * 70)
|
|
96
121
|
status_received.clear()
|
|
122
|
+
expected_command = CMD_STATUS_REQUEST
|
|
97
123
|
await mqtt_client.request_device_status(device)
|
|
98
124
|
|
|
99
125
|
try:
|
|
@@ -111,6 +137,7 @@ async def main() -> None:
|
|
|
111
137
|
print("STEP 2: Enabling Anti-Legionella cycle every 7 days...")
|
|
112
138
|
print("=" * 70)
|
|
113
139
|
status_received.clear()
|
|
140
|
+
expected_command = CMD_ANTI_LEGIONELLA_ENABLE
|
|
114
141
|
await mqtt_client.enable_anti_legionella(device, period_days=7)
|
|
115
142
|
|
|
116
143
|
try:
|
|
@@ -128,6 +155,7 @@ async def main() -> None:
|
|
|
128
155
|
print("WARNING: This reduces protection against Legionella bacteria!")
|
|
129
156
|
print("=" * 70)
|
|
130
157
|
status_received.clear()
|
|
158
|
+
expected_command = CMD_ANTI_LEGIONELLA_DISABLE
|
|
131
159
|
await mqtt_client.disable_anti_legionella(device)
|
|
132
160
|
|
|
133
161
|
try:
|
|
@@ -144,6 +172,7 @@ async def main() -> None:
|
|
|
144
172
|
print("STEP 4: Re-enabling Anti-Legionella with 14-day cycle...")
|
|
145
173
|
print("=" * 70)
|
|
146
174
|
status_received.clear()
|
|
175
|
+
expected_command = CMD_ANTI_LEGIONELLA_ENABLE
|
|
147
176
|
await mqtt_client.enable_anti_legionella(device, period_days=14)
|
|
148
177
|
|
|
149
178
|
try:
|
|
@@ -569,31 +569,23 @@ class NavienMqttClient(EventEmitter):
|
|
|
569
569
|
|
|
570
570
|
try:
|
|
571
571
|
# Build WebSocket MQTT connection with AWS credentials
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
on_connection_resumed=self._on_connection_resumed_internal,
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
# Run connection builder in thread pool to avoid blocking I/O
|
|
586
|
-
self._connection = await self._loop.run_in_executor(None, _build_connection)
|
|
572
|
+
self._connection = mqtt_connection_builder.websockets_with_default_aws_signing(
|
|
573
|
+
endpoint=self.config.endpoint,
|
|
574
|
+
region=self.config.region,
|
|
575
|
+
credentials_provider=self._create_credentials_provider(),
|
|
576
|
+
client_id=self.config.client_id,
|
|
577
|
+
clean_session=self.config.clean_session,
|
|
578
|
+
keep_alive_secs=self.config.keep_alive_secs,
|
|
579
|
+
on_connection_interrupted=self._on_connection_interrupted_internal,
|
|
580
|
+
on_connection_resumed=self._on_connection_resumed_internal,
|
|
581
|
+
)
|
|
587
582
|
|
|
588
583
|
# Connect
|
|
589
584
|
_logger.info("Establishing MQTT connection...")
|
|
590
585
|
|
|
591
|
-
#
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return connect_future.result()
|
|
595
|
-
|
|
596
|
-
connect_result = await self._loop.run_in_executor(None, _connect)
|
|
586
|
+
# Convert concurrent.futures.Future to asyncio.Future and await
|
|
587
|
+
connect_future = self._connection.connect()
|
|
588
|
+
connect_result = await asyncio.wrap_future(connect_future)
|
|
597
589
|
|
|
598
590
|
self._connected = True
|
|
599
591
|
self._reconnect_attempts = 0 # Reset on successful connection
|
|
@@ -644,12 +636,9 @@ class NavienMqttClient(EventEmitter):
|
|
|
644
636
|
await self.stop_all_periodic_tasks()
|
|
645
637
|
|
|
646
638
|
try:
|
|
647
|
-
#
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return disconnect_future.result()
|
|
651
|
-
|
|
652
|
-
await self._loop.run_in_executor(None, _disconnect)
|
|
639
|
+
# Convert concurrent.futures.Future to asyncio.Future and await
|
|
640
|
+
disconnect_future = self._connection.disconnect()
|
|
641
|
+
await asyncio.wrap_future(disconnect_future)
|
|
653
642
|
|
|
654
643
|
self._connected = False
|
|
655
644
|
self._connection = None
|
|
@@ -744,15 +733,11 @@ class NavienMqttClient(EventEmitter):
|
|
|
744
733
|
_logger.info(f"Subscribing to topic: {topic}")
|
|
745
734
|
|
|
746
735
|
try:
|
|
747
|
-
#
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
subscribe_result = subscribe_future.result()
|
|
753
|
-
return subscribe_result, packet_id
|
|
754
|
-
|
|
755
|
-
subscribe_result, packet_id = await self._loop.run_in_executor(None, _subscribe)
|
|
736
|
+
# Convert concurrent.futures.Future to asyncio.Future and await
|
|
737
|
+
subscribe_future, packet_id = self._connection.subscribe(
|
|
738
|
+
topic=topic, qos=qos, callback=self._on_message_received
|
|
739
|
+
)
|
|
740
|
+
subscribe_result = await asyncio.wrap_future(subscribe_future)
|
|
756
741
|
|
|
757
742
|
_logger.info(f"Subscribed to '{topic}' with QoS {subscribe_result['qos']}")
|
|
758
743
|
|
|
@@ -768,12 +753,18 @@ class NavienMqttClient(EventEmitter):
|
|
|
768
753
|
_logger.error(f"Failed to subscribe to '{_redact_topic(topic)}': {e}")
|
|
769
754
|
raise
|
|
770
755
|
|
|
771
|
-
async def unsubscribe(self, topic: str):
|
|
756
|
+
async def unsubscribe(self, topic: str) -> int:
|
|
772
757
|
"""
|
|
773
758
|
Unsubscribe from an MQTT topic.
|
|
774
759
|
|
|
775
760
|
Args:
|
|
776
761
|
topic: MQTT topic to unsubscribe from
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Unsubscribe packet ID
|
|
765
|
+
|
|
766
|
+
Raises:
|
|
767
|
+
Exception: If unsubscribe fails
|
|
777
768
|
"""
|
|
778
769
|
if not self._connected:
|
|
779
770
|
raise RuntimeError("Not connected to MQTT broker")
|
|
@@ -781,12 +772,9 @@ class NavienMqttClient(EventEmitter):
|
|
|
781
772
|
_logger.info(f"Unsubscribing from topic: {topic}")
|
|
782
773
|
|
|
783
774
|
try:
|
|
784
|
-
#
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
return unsubscribe_future.result()
|
|
788
|
-
|
|
789
|
-
await self._loop.run_in_executor(None, _unsubscribe)
|
|
775
|
+
# Convert concurrent.futures.Future to asyncio.Future and await
|
|
776
|
+
unsubscribe_future, packet_id = self._connection.unsubscribe(topic)
|
|
777
|
+
await asyncio.wrap_future(unsubscribe_future)
|
|
790
778
|
|
|
791
779
|
# Remove from tracking
|
|
792
780
|
self._subscriptions.pop(topic, None)
|
|
@@ -794,6 +782,8 @@ class NavienMqttClient(EventEmitter):
|
|
|
794
782
|
|
|
795
783
|
_logger.info(f"Unsubscribed from '{topic}'")
|
|
796
784
|
|
|
785
|
+
return packet_id
|
|
786
|
+
|
|
797
787
|
except Exception as e:
|
|
798
788
|
_logger.error(f"Failed to unsubscribe from '{_redact_topic(topic)}': {e}")
|
|
799
789
|
raise
|
|
@@ -835,15 +825,11 @@ class NavienMqttClient(EventEmitter):
|
|
|
835
825
|
# Serialize to JSON
|
|
836
826
|
payload_json = json.dumps(payload)
|
|
837
827
|
|
|
838
|
-
#
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
publish_future.result()
|
|
844
|
-
return packet_id
|
|
845
|
-
|
|
846
|
-
packet_id = await self._loop.run_in_executor(None, _publish)
|
|
828
|
+
# Convert concurrent.futures.Future to asyncio.Future and await
|
|
829
|
+
publish_future, packet_id = self._connection.publish(
|
|
830
|
+
topic=topic, payload=payload_json, qos=qos
|
|
831
|
+
)
|
|
832
|
+
await asyncio.wrap_future(publish_future)
|
|
847
833
|
|
|
848
834
|
_logger.debug(f"Published to '{topic}' with packet_id {packet_id}")
|
|
849
835
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|