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.
Files changed (100) hide show
  1. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/copilot-instructions.md +5 -0
  2. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/CHANGELOG.rst +10 -1
  3. {nwp500_python-1.2.0/src/nwp500_python.egg-info → nwp500_python-1.2.2}/PKG-INFO +1 -1
  4. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/MQTT_CLIENT.rst +3 -2
  5. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/anti_legionella_example.py +32 -3
  6. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/mqtt_client.py +38 -52
  7. {nwp500_python-1.2.0 → nwp500_python-1.2.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  8. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.coveragerc +0 -0
  9. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/workflows/ci.yml +0 -0
  10. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.github/workflows/release.yml +0 -0
  11. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.gitignore +0 -0
  12. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.pre-commit-config.yaml +0 -0
  13. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/.readthedocs.yml +0 -0
  14. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/AUTHORS.rst +0 -0
  15. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/CONTRIBUTING.rst +0 -0
  16. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/LICENSE.txt +0 -0
  17. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/Makefile +0 -0
  18. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/README.rst +0 -0
  19. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/RELEASE.md +0 -0
  20. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/API_CLIENT.rst +0 -0
  21. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/API_REFERENCE.rst +0 -0
  22. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTHENTICATION.rst +0 -0
  23. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTO_RECOVERY.rst +0 -0
  24. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/AUTO_RECOVERY_QUICK.rst +0 -0
  25. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/COMMAND_QUEUE.rst +0 -0
  26. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVELOPMENT.rst +0 -0
  27. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVICE_FEATURE_FIELDS.rst +0 -0
  28. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/DEVICE_STATUS_FIELDS.rst +0 -0
  29. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/ENERGY_MONITORING.rst +0 -0
  30. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/ERROR_CODES.rst +0 -0
  31. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/EVENT_EMITTER.rst +0 -0
  32. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/FIRMWARE_TRACKING.rst +0 -0
  33. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/MQTT_MESSAGES.rst +0 -0
  34. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/Makefile +0 -0
  35. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/_static/.gitignore +0 -0
  36. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/authors.rst +0 -0
  37. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/changelog.rst +0 -0
  38. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/conf.py +0 -0
  39. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/contributing.rst +0 -0
  40. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/index.rst +0 -0
  41. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/license.rst +0 -0
  42. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/openapi.yaml +0 -0
  43. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/readme.rst +0 -0
  44. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/docs/requirements.txt +0 -0
  45. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/.ruff.toml +0 -0
  46. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/README.md +0 -0
  47. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/api_client_example.py +0 -0
  48. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/auth_constructor_example.py +0 -0
  49. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/authenticate.py +0 -0
  50. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/auto_recovery_example.py +0 -0
  51. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/combined_callbacks.py +0 -0
  52. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/command_queue_demo.py +0 -0
  53. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_feature_callback.py +0 -0
  54. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_status_callback.py +0 -0
  55. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/device_status_callback_debug.py +0 -0
  56. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/energy_usage_example.py +0 -0
  57. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/event_emitter_demo.py +0 -0
  58. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/improved_auth_pattern.py +0 -0
  59. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/mask.py +0 -0
  60. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/mqtt_client_example.py +0 -0
  61. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/periodic_device_info.py +0 -0
  62. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/periodic_requests.py +0 -0
  63. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/power_control_example.py +0 -0
  64. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/reconnection_demo.py +0 -0
  65. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/reservation_schedule_example.py +0 -0
  66. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/set_dhw_temperature_example.py +0 -0
  67. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/set_mode_example.py +0 -0
  68. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_auto_recovery.py +0 -0
  69. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_periodic_info.py +0 -0
  70. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/simple_periodic_status.py +0 -0
  71. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_api_client.py +0 -0
  72. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_mqtt_connection.py +0 -0
  73. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_mqtt_messaging.py +0 -0
  74. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/test_periodic_minimal.py +0 -0
  75. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/examples/tou_schedule_example.py +0 -0
  76. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/pyproject.toml +0 -0
  77. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/format.py +0 -0
  78. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/lint.py +0 -0
  79. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/scripts/setup-dev.py +0 -0
  80. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/setup.cfg +0 -0
  81. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/setup.py +0 -0
  82. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/__init__.py +0 -0
  83. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/api_client.py +0 -0
  84. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/auth.py +0 -0
  85. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/cli.py +0 -0
  86. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/config.py +0 -0
  87. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/constants.py +0 -0
  88. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/events.py +0 -0
  89. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500/models.py +0 -0
  90. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  91. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  92. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  93. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  94. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  95. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  96. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/conftest.py +0 -0
  97. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_api_helpers.py +0 -0
  98. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_command_queue.py +0 -0
  99. {nwp500_python-1.2.0 → nwp500_python-1.2.2}/tests/test_events.py +0 -0
  100. {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.run_in_executor()``
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -1015,8 +1015,9 @@ async applications.
1015
1015
 
1016
1016
  **Implementation Details:**
1017
1017
 
1018
- - All AWS IoT SDK operations that could block are wrapped with ``asyncio.run_in_executor()``
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
- latest_status = status
77
- status_received.set()
78
- print("[DEBUG] Anti-Legionella status captured")
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
- # Run the connection building in a thread pool to avoid blocking I/O
573
- def _build_connection():
574
- return mqtt_connection_builder.websockets_with_default_aws_signing(
575
- endpoint=self.config.endpoint,
576
- region=self.config.region,
577
- credentials_provider=self._create_credentials_provider(),
578
- client_id=self.config.client_id,
579
- clean_session=self.config.clean_session,
580
- keep_alive_secs=self.config.keep_alive_secs,
581
- on_connection_interrupted=self._on_connection_interrupted_internal,
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
- # Run the connect operation in a thread pool to avoid blocking I/O
592
- def _connect():
593
- connect_future = self._connection.connect()
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
- # Run disconnect operation in thread pool to avoid blocking I/O
648
- def _disconnect():
649
- disconnect_future = self._connection.disconnect()
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
- # Run subscribe operation in thread pool to avoid blocking I/O
748
- def _subscribe():
749
- subscribe_future, packet_id = self._connection.subscribe(
750
- topic=topic, qos=qos, callback=self._on_message_received
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
- # Run unsubscribe operation in thread pool to avoid blocking I/O
785
- def _unsubscribe():
786
- unsubscribe_future, packet_id = self._connection.unsubscribe(topic)
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
- # Run publish operation in thread pool to avoid blocking I/O
839
- def _publish():
840
- publish_future, packet_id = self._connection.publish(
841
- topic=topic, payload=payload_json, qos=qos
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
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