nwp500-python 5.0.1__tar.gz → 5.0.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 (133) hide show
  1. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/CHANGELOG.rst +14 -0
  2. {nwp500_python-5.0.1/src/nwp500_python.egg-info → nwp500_python-5.0.2}/PKG-INFO +1 -1
  3. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_connection.py +72 -6
  4. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_subscriptions.py +28 -2
  5. {nwp500_python-5.0.1 → nwp500_python-5.0.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  6. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.coveragerc +0 -0
  7. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.github/copilot-instructions.md +0 -0
  8. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.github/workflows/ci.yml +0 -0
  9. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.github/workflows/release.yml +0 -0
  10. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.gitignore +0 -0
  11. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.pre-commit-config.yaml +0 -0
  12. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/.readthedocs.yml +0 -0
  13. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/AUTHORS.rst +0 -0
  14. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/CONTRIBUTING.rst +0 -0
  15. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/LICENSE.txt +0 -0
  16. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/Makefile +0 -0
  17. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/README.rst +0 -0
  18. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/RELEASE.md +0 -0
  19. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/Makefile +0 -0
  20. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/_static/.gitignore +0 -0
  21. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/authors.rst +0 -0
  22. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/changelog.rst +0 -0
  23. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/conf.py +0 -0
  24. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/configuration.rst +0 -0
  25. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/development/contributing.rst +0 -0
  26. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/development/history.rst +0 -0
  27. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/auto_recovery.rst +0 -0
  28. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/command_queue.rst +0 -0
  29. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/energy_monitoring.rst +0 -0
  30. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/event_system.rst +0 -0
  31. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/reservations.rst +0 -0
  32. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/guides/time_of_use.rst +0 -0
  33. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/index.rst +0 -0
  34. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/installation.rst +0 -0
  35. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/license.rst +0 -0
  36. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/openapi.yaml +0 -0
  37. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/device_features.rst +0 -0
  38. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/device_status.rst +0 -0
  39. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/error_codes.rst +0 -0
  40. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/firmware_tracking.rst +0 -0
  41. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/mqtt_protocol.rst +0 -0
  42. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/protocol/rest_api.rst +0 -0
  43. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/api_client.rst +0 -0
  44. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/auth_client.rst +0 -0
  45. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/cli.rst +0 -0
  46. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/constants.rst +0 -0
  47. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/events.rst +0 -0
  48. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/exceptions.rst +0 -0
  49. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/models.rst +0 -0
  50. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/python_api/mqtt_client.rst +0 -0
  51. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/quickstart.rst +0 -0
  52. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/docs/requirements.txt +0 -0
  53. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/.ruff.toml +0 -0
  54. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/README.md +0 -0
  55. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/anti_legionella_example.py +0 -0
  56. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/api_client_example.py +0 -0
  57. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/auth_constructor_example.py +0 -0
  58. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/authenticate.py +0 -0
  59. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/auto_recovery_example.py +0 -0
  60. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/combined_callbacks.py +0 -0
  61. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/command_queue_demo.py +0 -0
  62. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/device_feature_callback.py +0 -0
  63. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/device_status_callback.py +0 -0
  64. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/device_status_callback_debug.py +0 -0
  65. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/energy_usage_example.py +0 -0
  66. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/event_emitter_demo.py +0 -0
  67. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/exception_handling_example.py +0 -0
  68. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/improved_auth_pattern.py +0 -0
  69. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/mask.py +0 -0
  70. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/mqtt_client_example.py +0 -0
  71. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/periodic_device_info.py +0 -0
  72. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/periodic_requests.py +0 -0
  73. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/power_control_example.py +0 -0
  74. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/reconnection_demo.py +0 -0
  75. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/reservation_schedule_example.py +0 -0
  76. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/set_dhw_temperature_example.py +0 -0
  77. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/set_mode_example.py +0 -0
  78. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/simple_auto_recovery.py +0 -0
  79. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/simple_periodic_info.py +0 -0
  80. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/simple_periodic_status.py +0 -0
  81. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/test_api_client.py +0 -0
  82. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/test_mqtt_connection.py +0 -0
  83. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/test_mqtt_messaging.py +0 -0
  84. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/test_periodic_minimal.py +0 -0
  85. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/token_restoration_example.py +0 -0
  86. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/tou_openei_example.py +0 -0
  87. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/examples/tou_schedule_example.py +0 -0
  88. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/pyproject.toml +0 -0
  89. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/README.md +0 -0
  90. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/bump_version.py +0 -0
  91. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/format.py +0 -0
  92. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/lint.py +0 -0
  93. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/setup-dev.py +0 -0
  94. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/scripts/validate_version.py +0 -0
  95. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/setup.cfg +0 -0
  96. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/setup.py +0 -0
  97. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/__init__.py +0 -0
  98. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/api_client.py +0 -0
  99. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/auth.py +0 -0
  100. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/__init__.py +0 -0
  101. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/__main__.py +0 -0
  102. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/commands.py +0 -0
  103. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/monitoring.py +0 -0
  104. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/output_formatters.py +0 -0
  105. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/cli/token_storage.py +0 -0
  106. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/config.py +0 -0
  107. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/constants.py +0 -0
  108. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/encoding.py +0 -0
  109. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/events.py +0 -0
  110. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/exceptions.py +0 -0
  111. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/models.py +0 -0
  112. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_client.py +0 -0
  113. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_command_queue.py +0 -0
  114. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_device_control.py +0 -0
  115. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_periodic.py +0 -0
  116. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_reconnection.py +0 -0
  117. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/mqtt_utils.py +0 -0
  118. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/py.typed +0 -0
  119. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500/utils.py +0 -0
  120. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  121. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  122. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  123. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  124. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  125. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  126. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/conftest.py +0 -0
  127. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_api_helpers.py +0 -0
  128. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_auth.py +0 -0
  129. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_command_queue.py +0 -0
  130. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_events.py +0 -0
  131. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_exceptions.py +0 -0
  132. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tests/test_utils.py +0 -0
  133. {nwp500_python-5.0.1 → nwp500_python-5.0.2}/tox.ini +0 -0
@@ -2,6 +2,20 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 5.0.2 (2025-10-31)
6
+ ==========================
7
+
8
+ Fixed
9
+ -----
10
+
11
+ - **MQTT Future Cancellation**: Fixed InvalidStateError exceptions during disconnect
12
+
13
+ - Added asyncio.shield() to protect concurrent.futures.Future objects from cancellation
14
+ - Applied consistent cancellation handling across all MQTT operations (connect, disconnect, subscribe, unsubscribe, publish)
15
+ - AWS CRT callbacks can now complete independently without raising InvalidStateError
16
+ - Added debug logging when operations are cancelled for better diagnostics
17
+ - Ensures clean shutdown without spurious exception messages
18
+
5
19
  Version 5.0.1 (2025-10-27)
6
20
  ==========================
7
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 5.0.1
3
+ Version: 5.0.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
@@ -140,9 +140,23 @@ class MqttConnection:
140
140
  _logger.info("Establishing MQTT connection...")
141
141
 
142
142
  # Convert concurrent.futures.Future to asyncio.Future and await
143
+ # Use shield to prevent cancellation from propagating to
144
+ # underlying future
143
145
  if self._connection is not None:
144
146
  connect_future = self._connection.connect()
145
- connect_result = await asyncio.wrap_future(connect_future)
147
+ try:
148
+ connect_result = await asyncio.shield(
149
+ asyncio.wrap_future(connect_future)
150
+ )
151
+ except asyncio.CancelledError:
152
+ # Shield was cancelled - the underlying connect will
153
+ # complete independently, preventing InvalidStateError
154
+ # in AWS CRT callbacks
155
+ _logger.debug(
156
+ "Connect operation was cancelled but will complete "
157
+ "in background"
158
+ )
159
+ raise
146
160
  else:
147
161
  raise MqttConnectionError("Connection not initialized")
148
162
 
@@ -196,8 +210,20 @@ class MqttConnection:
196
210
 
197
211
  try:
198
212
  # Convert concurrent.futures.Future to asyncio.Future and await
213
+ # Use shield to prevent cancellation from propagating to
214
+ # underlying future
199
215
  disconnect_future = self._connection.disconnect()
200
- await asyncio.wrap_future(disconnect_future)
216
+ try:
217
+ await asyncio.shield(asyncio.wrap_future(disconnect_future))
218
+ except asyncio.CancelledError:
219
+ # Shield was cancelled - the underlying disconnect will
220
+ # complete independently, preventing InvalidStateError
221
+ # in AWS CRT callbacks
222
+ _logger.debug(
223
+ "Disconnect operation was cancelled but will complete "
224
+ "in background"
225
+ )
226
+ raise
201
227
 
202
228
  self._connected = False
203
229
  self._connection = None
@@ -232,10 +258,22 @@ class MqttConnection:
232
258
  _logger.debug(f"Subscribing to topic: {topic}")
233
259
 
234
260
  # Convert concurrent.futures.Future to asyncio.Future and await
261
+ # Use shield to prevent cancellation from propagating to
262
+ # underlying future
235
263
  subscribe_future, packet_id = self._connection.subscribe(
236
264
  topic=topic, qos=qos, callback=callback
237
265
  )
238
- await asyncio.wrap_future(subscribe_future)
266
+ try:
267
+ await asyncio.shield(asyncio.wrap_future(subscribe_future))
268
+ except asyncio.CancelledError:
269
+ # Shield was cancelled - the underlying subscribe will
270
+ # complete independently, preventing InvalidStateError
271
+ # in AWS CRT callbacks
272
+ _logger.debug(
273
+ f"Subscribe to '{topic}' was cancelled but will complete "
274
+ "in background"
275
+ )
276
+ raise
239
277
 
240
278
  _logger.info(f"Subscribed to '{topic}' with packet_id {packet_id}")
241
279
  return (subscribe_future, packet_id)
@@ -259,10 +297,22 @@ class MqttConnection:
259
297
  _logger.debug(f"Unsubscribing from topic: {topic}")
260
298
 
261
299
  # Convert concurrent.futures.Future to asyncio.Future and await
300
+ # Use shield to prevent cancellation from propagating to
301
+ # underlying future
262
302
  unsubscribe_future, packet_id = self._connection.unsubscribe(
263
303
  topic=topic
264
304
  )
265
- await asyncio.wrap_future(unsubscribe_future)
305
+ try:
306
+ await asyncio.shield(asyncio.wrap_future(unsubscribe_future))
307
+ except asyncio.CancelledError:
308
+ # Shield was cancelled - the underlying unsubscribe will
309
+ # complete independently, preventing InvalidStateError
310
+ # in AWS CRT callbacks
311
+ _logger.debug(
312
+ f"Unsubscribe from '{topic}' was cancelled but will "
313
+ "complete in background"
314
+ )
315
+ raise
266
316
 
267
317
  _logger.info(f"Unsubscribed from '{topic}' with packet_id {packet_id}")
268
318
  return int(packet_id)
@@ -286,6 +336,7 @@ class MqttConnection:
286
336
 
287
337
  Raises:
288
338
  RuntimeError: If not connected
339
+ asyncio.CancelledError: If operation cancelled during disconnect
289
340
  """
290
341
  if not self._connected or not self._connection:
291
342
  raise MqttNotConnectedError("Not connected to MQTT broker")
@@ -303,11 +354,26 @@ class MqttConnection:
303
354
  # Try to JSON encode other types
304
355
  payload_bytes = json.dumps(payload).encode("utf-8")
305
356
 
306
- # Convert concurrent.futures.Future to asyncio.Future and await
357
+ # Publish and get the concurrent.futures.Future
307
358
  publish_future, packet_id = self._connection.publish(
308
359
  topic=topic, payload=payload_bytes, qos=qos
309
360
  )
310
- await asyncio.wrap_future(publish_future)
361
+
362
+ # Shield the operation to prevent cancellation from propagating to
363
+ # the underlying concurrent.futures.Future. This avoids
364
+ # InvalidStateError when AWS CRT tries to set exception on a
365
+ # cancelled future.
366
+ try:
367
+ await asyncio.shield(asyncio.wrap_future(publish_future))
368
+ except asyncio.CancelledError:
369
+ # Shield was cancelled - the underlying publish will complete
370
+ # independently, preventing InvalidStateError in AWS CRT
371
+ # callbacks
372
+ _logger.debug(
373
+ f"Publish to '{topic}' was cancelled but will complete "
374
+ "in background"
375
+ )
376
+ raise
311
377
 
312
378
  _logger.debug(f"Published to '{topic}' with packet_id {packet_id}")
313
379
  return int(packet_id)
@@ -214,10 +214,24 @@ class MqttSubscriptionManager:
214
214
 
215
215
  try:
216
216
  # Convert concurrent.futures.Future to asyncio.Future and await
217
+ # Use shield to prevent cancellation from propagating to
218
+ # underlying future
217
219
  subscribe_future, packet_id = self._connection.subscribe(
218
220
  topic=topic, qos=qos, callback=self._on_message_received
219
221
  )
220
- subscribe_result = await asyncio.wrap_future(subscribe_future)
222
+ try:
223
+ subscribe_result = await asyncio.shield(
224
+ asyncio.wrap_future(subscribe_future)
225
+ )
226
+ except asyncio.CancelledError:
227
+ # Shield was cancelled - the underlying subscribe will
228
+ # complete independently, preventing InvalidStateError
229
+ # in AWS CRT callbacks
230
+ _logger.debug(
231
+ f"Subscribe to '{redact_topic(topic)}' was cancelled "
232
+ "but will complete in background"
233
+ )
234
+ raise
221
235
 
222
236
  _logger.info(
223
237
  f"Subscription succeeded (topic redacted) with QoS "
@@ -259,8 +273,20 @@ class MqttSubscriptionManager:
259
273
 
260
274
  try:
261
275
  # Convert concurrent.futures.Future to asyncio.Future and await
276
+ # Use shield to prevent cancellation from propagating to
277
+ # underlying future
262
278
  unsubscribe_future, packet_id = self._connection.unsubscribe(topic)
263
- await asyncio.wrap_future(unsubscribe_future)
279
+ try:
280
+ await asyncio.shield(asyncio.wrap_future(unsubscribe_future))
281
+ except asyncio.CancelledError:
282
+ # Shield was cancelled - the underlying unsubscribe will
283
+ # complete independently, preventing InvalidStateError
284
+ # in AWS CRT callbacks
285
+ _logger.debug(
286
+ f"Unsubscribe from '{redact_topic(topic)}' was "
287
+ "cancelled but will complete in background"
288
+ )
289
+ raise
264
290
 
265
291
  # Remove from tracking
266
292
  self._subscriptions.pop(topic, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 5.0.1
3
+ Version: 5.0.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