nwp500-python 7.4.8__tar.gz → 7.4.9__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 (183) hide show
  1. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/CHANGELOG.rst +84 -0
  2. {nwp500_python-7.4.8/src/nwp500_python.egg-info → nwp500_python-7.4.9}/PKG-INFO +6 -6
  3. nwp500_python-7.4.9/examples/advanced/firmware_payload_capture.py +211 -0
  4. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/setup.cfg +5 -5
  5. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/auth.py +35 -6
  6. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/__main__.py +1 -1
  7. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/handlers.py +6 -5
  8. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/events.py +2 -2
  9. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/factory.py +14 -5
  10. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/control.py +6 -3
  11. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/subscriptions.py +30 -14
  12. {nwp500_python-7.4.8 → nwp500_python-7.4.9/src/nwp500_python.egg-info}/PKG-INFO +6 -6
  13. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
  14. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/requires.txt +5 -5
  15. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_auth.py +15 -14
  16. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_mqtt_client_init.py +10 -10
  17. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.coveragerc +0 -0
  18. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  19. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.github/copilot-instructions.md +0 -0
  20. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.github/workflows/ci.yml +0 -0
  21. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.github/workflows/release.yml +0 -0
  22. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.gitignore +0 -0
  23. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.pre-commit-config.yaml +0 -0
  24. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/.readthedocs.yml +0 -0
  25. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/AUTHORS.rst +0 -0
  26. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/CONTRIBUTING.rst +0 -0
  27. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/LICENSE.txt +0 -0
  28. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/Makefile +0 -0
  29. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/README.rst +0 -0
  30. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/RELEASE.md +0 -0
  31. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/Makefile +0 -0
  32. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/_static/.gitignore +0 -0
  33. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/api/nwp500.rst +0 -0
  34. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/authors.rst +0 -0
  35. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/changelog.rst +0 -0
  36. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/conf.py +0 -0
  37. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/configuration.rst +0 -0
  38. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/development/contributing.rst +0 -0
  39. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/development/history.rst +0 -0
  40. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/enumerations.rst +0 -0
  41. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/advanced_features_explained.rst +0 -0
  42. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/authentication.rst +0 -0
  43. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/auto_recovery.rst +0 -0
  44. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/command_queue.rst +0 -0
  45. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/energy_monitoring.rst +0 -0
  46. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/event_system.rst +0 -0
  47. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/home_assistant_integration.rst +0 -0
  48. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/mqtt_diagnostics.rst +0 -0
  49. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/scheduling.rst +0 -0
  50. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/time_of_use.rst +0 -0
  51. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/guides/unit_conversion.rst +0 -0
  52. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/index.rst +0 -0
  53. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/installation.rst +0 -0
  54. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/license.rst +0 -0
  55. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/openapi.yaml +0 -0
  56. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/data_conversions.rst +0 -0
  57. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/device_features.rst +0 -0
  58. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/device_status.rst +0 -0
  59. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/error_codes.rst +0 -0
  60. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/mqtt_protocol.rst +0 -0
  61. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/quick_reference.rst +0 -0
  62. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/protocol/rest_api.rst +0 -0
  63. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/api_client.rst +0 -0
  64. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/auth_client.rst +0 -0
  65. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/cli.rst +0 -0
  66. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/device_control.rst +0 -0
  67. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/events.rst +0 -0
  68. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/exceptions.rst +0 -0
  69. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/models.rst +0 -0
  70. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/python_api/mqtt_client.rst +0 -0
  71. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/quickstart.rst +0 -0
  72. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/docs/requirements.txt +0 -0
  73. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/.ruff.toml +0 -0
  74. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/README.md +0 -0
  75. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/air_filter_reset.py +0 -0
  76. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/anti_legionella.py +0 -0
  77. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/auto_recovery.py +0 -0
  78. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/combined_callbacks.py +0 -0
  79. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/demand_response.py +0 -0
  80. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/device_capabilities.py +0 -0
  81. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/device_status_debug.py +0 -0
  82. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/energy_analytics.py +0 -0
  83. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/error_code_demo.py +0 -0
  84. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/mqtt_diagnostics.py +0 -0
  85. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/power_control.py +0 -0
  86. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/recirculation_control.py +0 -0
  87. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/reconnection_demo.py +0 -0
  88. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/reservation_schedule.py +0 -0
  89. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/simple_auto_recovery.py +0 -0
  90. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/token_restoration.py +0 -0
  91. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/tou_openei.py +0 -0
  92. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/tou_schedule.py +0 -0
  93. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/advanced/water_reservation.py +0 -0
  94. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/beginner/01_authentication.py +0 -0
  95. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/beginner/02_list_devices.py +0 -0
  96. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/beginner/03_get_status.py +0 -0
  97. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/beginner/04_set_temperature.py +0 -0
  98. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/advanced_auth_patterns.py +0 -0
  99. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/command_queue.py +0 -0
  100. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/device_status_callback.py +0 -0
  101. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/error_handling.py +0 -0
  102. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/event_driven_control.py +0 -0
  103. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/improved_auth.py +0 -0
  104. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/legacy_auth_constructor.py +0 -0
  105. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  106. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/periodic_requests.py +0 -0
  107. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/set_mode.py +0 -0
  108. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/intermediate/vacation_mode.py +0 -0
  109. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/mask.py +0 -0
  110. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/periodic_device_info.py +0 -0
  111. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/simple_periodic_info.py +0 -0
  112. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/test_api_client.py +0 -0
  113. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/test_mqtt_connection.py +0 -0
  114. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/test_mqtt_messaging.py +0 -0
  115. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/examples/testing/test_periodic_minimal.py +0 -0
  116. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/pyproject.toml +0 -0
  117. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/README.md +0 -0
  118. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/bump_version.py +0 -0
  119. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/diagnose_mqtt_connection.py +0 -0
  120. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/extract_changelog.py +0 -0
  121. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/format.py +0 -0
  122. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/lint.py +0 -0
  123. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/setup-dev.py +0 -0
  124. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/scripts/validate_version.py +0 -0
  125. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/setup.py +0 -0
  126. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/__init__.py +0 -0
  127. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/api_client.py +0 -0
  128. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/__init__.py +0 -0
  129. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/commands.py +0 -0
  130. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/monitoring.py +0 -0
  131. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/output_formatters.py +0 -0
  132. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/rich_output.py +0 -0
  133. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/cli/token_storage.py +0 -0
  134. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/command_decorators.py +0 -0
  135. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/config.py +0 -0
  136. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/converters.py +0 -0
  137. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/device_capabilities.py +0 -0
  138. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/device_info_cache.py +0 -0
  139. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/encoding.py +0 -0
  140. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/enums.py +0 -0
  141. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/exceptions.py +0 -0
  142. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/field_factory.py +0 -0
  143. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/models.py +0 -0
  144. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/__init__.py +0 -0
  145. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/client.py +0 -0
  146. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/command_queue.py +0 -0
  147. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/connection.py +0 -0
  148. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/diagnostics.py +0 -0
  149. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/periodic.py +0 -0
  150. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/reconnection.py +0 -0
  151. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt/utils.py +0 -0
  152. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/mqtt_events.py +0 -0
  153. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/openei.py +0 -0
  154. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/py.typed +0 -0
  155. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/reservations.py +0 -0
  156. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/temperature.py +0 -0
  157. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/topic_builder.py +0 -0
  158. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/unit_system.py +0 -0
  159. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500/utils.py +0 -0
  160. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  161. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  162. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  163. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/src/nwp500_python.egg-info/top_level.txt +0 -0
  164. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/conftest.py +0 -0
  165. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_api_helpers.py +0 -0
  166. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_cli_basic.py +0 -0
  167. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_cli_commands.py +0 -0
  168. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_command_decorators.py +0 -0
  169. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_command_queue.py +0 -0
  170. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_device_capabilities.py +0 -0
  171. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_device_info_cache.py +0 -0
  172. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_events.py +0 -0
  173. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_exceptions.py +0 -0
  174. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_model_converters.py +0 -0
  175. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_models.py +0 -0
  176. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_mqtt_hypothesis.py +0 -0
  177. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_openei.py +0 -0
  178. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_reservations.py +0 -0
  179. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_temperature_converters.py +0 -0
  180. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_tou_api.py +0 -0
  181. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_unit_switching.py +0 -0
  182. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tests/test_utils.py +0 -0
  183. {nwp500_python-7.4.8 → nwp500_python-7.4.9}/tox.ini +0 -0
@@ -2,6 +2,90 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 7.4.9 (2026-04-12)
6
+ ==========================
7
+
8
+ Added
9
+ -----
10
+ - **Firmware Payload Capture Tool**: New example script
11
+ ``examples/advanced/firmware_payload_capture.py`` for capturing raw MQTT
12
+ payloads to detect firmware-introduced protocol changes. Subscribes to all
13
+ response and event topics via wildcards, requests the full scheduling
14
+ data set (weekly reservations, TOU, device info), and saves everything to a
15
+ timestamped JSON file suitable for ``jq``/``diff`` comparison across firmware
16
+ versions.
17
+
18
+ Fixed
19
+ -----
20
+ - **Timezone-naive datetime in token expiry checks**: ``AuthTokens.is_expired``,
21
+ ``are_aws_credentials_expired``, and ``time_until_expiry`` used
22
+ ``datetime.now()`` (naive, local time). During DST transitions or timezone
23
+ changes this could cause incorrect expiry detection, leading to premature
24
+ re-authentication or use of an actually-expired token. Fixed by using
25
+ ``datetime.now(UTC)`` throughout, switching the ``issued_at`` field default
26
+ to ``datetime.now(UTC)``, and adding a field validator to normalize any
27
+ timezone-naive ``issued_at`` values loaded from old stored token files to UTC
28
+ (previously this would raise a ``TypeError`` at comparison time). The
29
+ validator was further extended to also handle ISO 8601 strings without
30
+ timezone info (e.g. ``"2026-02-17T14:47:01.686943"``), which is the actual
31
+ format written by ``to_dict()`` for tokens stored before this fix.
32
+ - **Vacation mode sent wrong MQTT command**: ``set_vacation_days()`` used
33
+ ``CommandCode.GOOUT_DAY`` (33554466), which the device silently accepted
34
+ but did not activate vacation mode — the operating mode remained unchanged.
35
+ HAR capture of the official Navien app confirms the correct command is
36
+ ``DHW_MODE`` (33554437) with ``param=[5, days]``
37
+ (``DhwOperationSetting.VACATION``). The valid range has also been corrected
38
+ from 1–365 to 1–30 to match the device's actual constraint.
39
+ - **Duplicate AWS IoT subscribe calls on reconnect**: ``resubscribe_all()``
40
+ called ``connection.subscribe()`` (a network round-trip to AWS IoT) once per
41
+ handler per topic. If a topic had N handlers, N identical subscribe requests
42
+ were sent on every reconnect. Fixed by making one network call per unique
43
+ topic and registering remaining handlers directly into ``_message_handlers``.
44
+ - **Anti-Legionella set-period State Preservation**: ``nwp-cli anti-legionella
45
+ set-period`` was calling ``enable_anti_legionella()`` in both the enabled and
46
+ disabled branches, silently re-enabling the feature when it was off. The
47
+ command now informs the user that the period can only be updated while the
48
+ feature is enabled and directs them to ``anti-legionella enable``.
49
+ - **Subscription State Lost After Failed Resubscription**: ``resubscribe_all()``
50
+ cleared ``_subscriptions`` and ``_message_handlers`` before the re-subscribe
51
+ loop. Topics that failed to resubscribe were permanently dropped from internal
52
+ state and could not be retried on the next reconnection. Failed topics are now
53
+ restored so they are retried automatically.
54
+ - **Unit System Detection Returns None on Timeout**: ``_detect_unit_system()``
55
+ declared return type ``UnitSystemType`` but returned ``None`` on
56
+ ``TimeoutError``, violating the type contract. Now returns
57
+ ``"us_customary"`` consistent with the warning message.
58
+ - **Once-Listener Becomes Permanent With Duplicate Callbacks**: ``emit()``
59
+ identified once-listeners via a ``set`` of ``(event, callback)`` tuples. If
60
+ the same callback was registered twice with ``once=True``, the set
61
+ deduplicated the tuple — after the first emit the second listener lost its
62
+ once-status and became permanent. Fixed by checking ``listener.once``
63
+ directly on the ``EventListener`` object.
64
+ - **Auth Session Leaked on Client Construction Failure**: In
65
+ ``create_navien_clients()``, if ``NavienAPIClient`` or
66
+ ``NavienMqttClient`` construction raised after a successful
67
+ ``auth_client.__aenter__()``, the auth session and its underlying
68
+ ``aiohttp`` session would leak. Client construction is now wrapped in a
69
+ ``try/except`` that calls ``auth_client.__aexit__()`` on failure.
70
+ Additionally, both ``except BaseException`` blocks have been replaced with
71
+ ``except Exception`` (passing real exception info to ``__aexit__``) plus a
72
+ separate ``except asyncio.CancelledError`` block that uses
73
+ ``asyncio.shield()`` to ensure cleanup completes even when the task is
74
+ being cancelled.
75
+ - **Hypothesis Tests Broke All Test Collection**: ``test_mqtt_hypothesis.py``
76
+ imported ``hypothesis`` at module level; when it was not installed, pytest
77
+ failed to collect every test in the suite. ``hypothesis`` is now mandated
78
+ as a ``[testing]`` extra dependency, restoring correct collection behaviour.
79
+
80
+ Changed
81
+ -------
82
+ - **Dependency updates**: Bumped minimum versions to track current releases:
83
+ ``aiohttp >= 3.13.5``, ``pydantic >= 2.12.5``, ``click >= 8.3.0``,
84
+ ``rich >= 14.3.0``.
85
+ - **Dependency: awsiotsdk >= 1.28.2**: Bumped minimum ``awsiotsdk`` version
86
+ from ``>=1.27.0`` to ``>=1.28.2`` to track the current patch release.
87
+ ``awscrt`` 0.31.3 is pulled in transitively.
88
+
5
89
  Version 7.4.8 (2026-02-17)
6
90
  ==========================
7
91
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 7.4.8
3
+ Version: 7.4.9
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
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Requires-Python: >=3.13
19
19
  Description-Content-Type: text/x-rst; charset=UTF-8
20
20
  License-File: LICENSE.txt
21
- Requires-Dist: aiohttp>=3.8.0
22
- Requires-Dist: awsiotsdk>=1.27.0
23
- Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: aiohttp>=3.13.5
22
+ Requires-Dist: awsiotsdk>=1.28.2
23
+ Requires-Dist: pydantic>=2.12.5
24
24
  Provides-Extra: cli
25
- Requires-Dist: click>=8.0.0; extra == "cli"
26
- Requires-Dist: rich>=13.0.0; extra == "cli"
25
+ Requires-Dist: click>=8.3.0; extra == "cli"
26
+ Requires-Dist: rich>=14.3.0; extra == "cli"
27
27
  Provides-Extra: testing
28
28
  Requires-Dist: setuptools; extra == "testing"
29
29
  Requires-Dist: pytest; extra == "testing"
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Firmware Payload Capture Tool.
4
+
5
+ Captures raw MQTT payloads for all scheduling-related topics and dumps them
6
+ to a timestamped JSON file. Use this to detect changes introduced by firmware
7
+ updates by diffing captures taken before and after an update.
8
+
9
+ Specifically captures:
10
+ - Weekly reservations (rsv/rd)
11
+ - Time-of-Use schedule (tou/rd)
12
+ - Device info (firmware versions, capabilities)
13
+ - Device status (current operating state)
14
+ - All other response/event topics (via wildcards)
15
+
16
+ Usage:
17
+ NAVIEN_EMAIL=your@email.com NAVIEN_PASSWORD=password python3 firmware_payload_capture.py
18
+
19
+ Output:
20
+ payload_capture_YYYYMMDD_HHMMSS.json — all captured payloads with topics
21
+ and timestamps. Sensitive fields
22
+ (MAC address, session IDs, client
23
+ IDs) are redacted in the output.
24
+
25
+ Comparing two captures to find firmware changes:
26
+ diff <(jq '.payloads[] | select(.topic | contains("rsv"))' before.json) \\
27
+ <(jq '.payloads[] | select(.topic | contains("rsv"))' after.json)
28
+ """
29
+
30
+ import asyncio
31
+ import json
32
+ import logging
33
+ import os
34
+ import sys
35
+ from datetime import UTC, datetime
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
40
+ from nwp500.models import DeviceFeature
41
+ from nwp500.mqtt.utils import redact, redact_topic
42
+ from nwp500.topic_builder import MqttTopicBuilder
43
+
44
+ logging.basicConfig(
45
+ level=logging.WARNING,
46
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
47
+ )
48
+ _logger = logging.getLogger(__name__)
49
+
50
+
51
+ class PayloadCapture:
52
+ """Captures and records raw MQTT payloads."""
53
+
54
+ def __init__(self) -> None:
55
+ self.payloads: list[dict[str, Any]] = []
56
+
57
+ def record(self, topic: str, message: dict[str, Any]) -> None:
58
+ entry = {
59
+ "timestamp": datetime.now(UTC).isoformat(),
60
+ "topic": topic,
61
+ "payload": message,
62
+ }
63
+ self.payloads.append(entry)
64
+ print(f" ← {redact_topic(topic)}")
65
+
66
+ def save(self, path: Path) -> None:
67
+ # Redact sensitive fields (MAC, session IDs, client IDs) before saving
68
+ # so the output file is safe to share. Protocol structure and payload
69
+ # field values used for firmware analysis are preserved.
70
+ redacted_payloads = [
71
+ {
72
+ "timestamp": e["timestamp"],
73
+ "topic": redact_topic(e["topic"]),
74
+ "payload": redact(e["payload"]),
75
+ }
76
+ for e in self.payloads
77
+ ]
78
+ data = {
79
+ "captured_at": datetime.now(UTC).isoformat(),
80
+ "total_payloads": len(self.payloads),
81
+ "payloads": redacted_payloads,
82
+ }
83
+ path.write_text(json.dumps(data, indent=2, default=str))
84
+ print(f"\nSaved {len(self.payloads)} payloads → {path}")
85
+
86
+
87
+ async def main() -> None:
88
+ email = os.getenv("NAVIEN_EMAIL")
89
+ password = os.getenv("NAVIEN_PASSWORD")
90
+
91
+ if not email or not password:
92
+ print("Error: set NAVIEN_EMAIL and NAVIEN_PASSWORD environment variables")
93
+ sys.exit(1)
94
+
95
+ capture = PayloadCapture()
96
+
97
+ async with NavienAuthClient(email, password) as auth_client:
98
+ api_client = NavienAPIClient(auth_client=auth_client)
99
+ device = await api_client.get_first_device()
100
+ if not device:
101
+ print("No devices found for this account")
102
+ return
103
+
104
+ device_type = str(device.device_info.device_type)
105
+ mac = device.device_info.mac_address
106
+ print(f"Device: {device.device_info.device_name} [{device_type}]")
107
+
108
+ mqtt_client = NavienMqttClient(auth_client)
109
+ await mqtt_client.connect()
110
+
111
+ client_id = mqtt_client.client_id
112
+
113
+ # --- Wildcard subscriptions to catch everything ---
114
+
115
+ # All response messages back to this client
116
+ res_wildcard = MqttTopicBuilder.response_topic(device_type, client_id, "#")
117
+ # All event messages pushed by the device
118
+ evt_wildcard = MqttTopicBuilder.event_topic(device_type, mac, "#")
119
+
120
+ print(
121
+ f"\nSubscribing to:\n {redact_topic(res_wildcard)}\n"
122
+ f" {redact_topic(evt_wildcard)}\n"
123
+ )
124
+ print("Captured topics:")
125
+
126
+ await mqtt_client.subscribe(res_wildcard, capture.record)
127
+ await mqtt_client.subscribe(evt_wildcard, capture.record)
128
+
129
+ # --- Step 1: fetch device info (needed for firmware version + serial) ---
130
+ device_info_event: asyncio.Event = asyncio.Event()
131
+ device_feature: DeviceFeature | None = None
132
+
133
+ def on_feature(feature: DeviceFeature) -> None:
134
+ nonlocal device_feature
135
+ device_feature = feature
136
+ device_info_event.set()
137
+
138
+ await mqtt_client.subscribe_device_feature(device, on_feature)
139
+ await mqtt_client.control.request_device_info(device)
140
+ await asyncio.wait_for(device_info_event.wait(), timeout=30.0)
141
+
142
+ if device_feature:
143
+ print(
144
+ f"\nFirmware: controller={device_feature.controller_sw_version} "
145
+ f"panel={device_feature.panel_sw_version} "
146
+ f"wifi={device_feature.wifi_sw_version}"
147
+ )
148
+
149
+ # --- Step 2: request device status ---
150
+ await mqtt_client.control.request_device_status(device)
151
+ await asyncio.sleep(3)
152
+
153
+ # --- Step 3: request reservation (weekly) schedule ---
154
+ print("\nRequesting weekly reservation schedule...")
155
+ await mqtt_client.control.request_reservations(device)
156
+ await asyncio.sleep(5)
157
+
158
+ # --- Step 4: request TOU schedule (requires controller serial number) ---
159
+ if device_feature and device_feature.program_reservation_use:
160
+ serial = device_feature.controller_serial_number
161
+ if serial:
162
+ print("Requesting TOU schedule...")
163
+ try:
164
+ await mqtt_client.control.request_tou_settings(device, serial)
165
+ await asyncio.sleep(5)
166
+ except Exception as exc:
167
+ print(f" TOU request failed: {exc}")
168
+
169
+ # --- Step 5: wait a bit more to catch any late-arriving messages ---
170
+ print("\nWaiting for any remaining messages...")
171
+ await asyncio.sleep(5)
172
+
173
+ await mqtt_client.disconnect()
174
+
175
+ # --- Save results ---
176
+ timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
177
+ output_path = Path(f"payload_capture_{timestamp}.json")
178
+ capture.save(output_path)
179
+
180
+ # Print a summary grouped by topic
181
+ print("\n--- Summary by topic ---")
182
+ by_topic: dict[str, int] = {}
183
+ for entry in capture.payloads:
184
+ by_topic[entry["topic"]] = by_topic.get(entry["topic"], 0) + 1
185
+ for topic, count in sorted(by_topic.items()):
186
+ print(f" {count:2d}x {redact_topic(topic)}")
187
+
188
+ if device_feature:
189
+ print(
190
+ f"\nFirmware captured: controller_sw_version="
191
+ f"{device_feature.controller_sw_version}"
192
+ )
193
+ print(
194
+ "Compare this file against a capture from a different firmware version "
195
+ "to detect scheduling changes.\n"
196
+ "Useful diff command:\n"
197
+ " diff <(jq '.payloads[] | select(.topic | contains(\"rsv\"))' "
198
+ f"before.json) \\\n"
199
+ " <(jq '.payloads[] | select(.topic | contains(\"rsv\"))' "
200
+ f"{output_path})"
201
+ )
202
+
203
+
204
+ if __name__ == "__main__":
205
+ try:
206
+ asyncio.run(main())
207
+ except KeyboardInterrupt:
208
+ print("\nCancelled by user")
209
+ except TimeoutError:
210
+ print("\nError: timed out waiting for device response. Is the device online?")
211
+ sys.exit(1)
@@ -28,9 +28,9 @@ package_dir =
28
28
  =src
29
29
  python_requires = >=3.13
30
30
  install_requires =
31
- aiohttp>=3.8.0
32
- awsiotsdk>=1.27.0
33
- pydantic>=2.0.0
31
+ aiohttp>=3.13.5
32
+ awsiotsdk>=1.28.2
33
+ pydantic>=2.12.5
34
34
 
35
35
  [options.packages.find]
36
36
  where = src
@@ -39,8 +39,8 @@ exclude =
39
39
 
40
40
  [options.extras_require]
41
41
  cli =
42
- click>=8.0.0
43
- rich>=13.0.0
42
+ click>=8.3.0
43
+ rich>=14.3.0
44
44
  testing =
45
45
  setuptools
46
46
  pytest
@@ -15,11 +15,18 @@ from __future__ import annotations
15
15
 
16
16
  import json
17
17
  import logging
18
- from datetime import datetime, timedelta
18
+ from datetime import UTC, datetime, timedelta
19
19
  from typing import Any, Literal, Self, cast
20
20
 
21
21
  import aiohttp
22
- from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
22
+ from pydantic import (
23
+ BaseModel,
24
+ ConfigDict,
25
+ Field,
26
+ PrivateAttr,
27
+ field_validator,
28
+ model_validator,
29
+ )
23
30
  from pydantic.alias_generators import to_camel
24
31
 
25
32
  from . import __version__
@@ -79,11 +86,31 @@ class AuthTokens(NavienBaseModel):
79
86
  authorization_expires_in: int | None = None
80
87
 
81
88
  # Calculated fields
82
- issued_at: datetime = Field(default_factory=datetime.now)
89
+ issued_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
83
90
 
84
91
  _expires_at: datetime = PrivateAttr()
85
92
  _aws_expires_at: datetime | None = PrivateAttr(default=None)
86
93
 
94
+ @field_validator("issued_at", mode="before")
95
+ @classmethod
96
+ def _normalize_issued_at_tz(cls, v: Any) -> Any:
97
+ """Assume UTC for timezone-naive datetimes.
98
+
99
+ Handles old stored tokens that may not have timezone info,
100
+ whether provided as a datetime object or an ISO 8601 string.
101
+ """
102
+ if isinstance(v, str) and not v.endswith("Z"):
103
+ # Check for a timezone offset (+HH:MM or -HH:MM) in the time
104
+ # portion only (after the 'T' separator), so that date-part hyphens
105
+ # like "2026-02-17" are not mistaken for a negative offset.
106
+ t_pos = v.find("T")
107
+ time_part = v[t_pos + 1 :] if t_pos >= 0 else v
108
+ if "+" not in time_part and "-" not in time_part:
109
+ return v + "+00:00"
110
+ if isinstance(v, datetime) and v.tzinfo is None:
111
+ return v.replace(tzinfo=UTC)
112
+ return v
113
+
87
114
  @model_validator(mode="before")
88
115
  @classmethod
89
116
  def handle_empty_aliases(cls, data: Any) -> Any:
@@ -159,7 +186,7 @@ class AuthTokens(NavienBaseModel):
159
186
  def is_expired(self) -> bool:
160
187
  """Check if the access token has expired (cached calculation)."""
161
188
  # Consider expired if within 5 minutes of expiration
162
- return datetime.now() >= (self._expires_at - timedelta(minutes=5))
189
+ return datetime.now(UTC) >= (self._expires_at - timedelta(minutes=5))
163
190
 
164
191
  @property
165
192
  def are_aws_credentials_expired(self) -> bool:
@@ -178,7 +205,9 @@ class AuthTokens(NavienBaseModel):
178
205
  # This handles cases where authorization_expires_in wasn't provided
179
206
  return False
180
207
  # Consider expired if within 5 minutes of expiration
181
- return datetime.now() >= (self._aws_expires_at - timedelta(minutes=5))
208
+ return datetime.now(UTC) >= (
209
+ self._aws_expires_at - timedelta(minutes=5)
210
+ )
182
211
 
183
212
  @property
184
213
  def time_until_expiry(self) -> timedelta:
@@ -186,7 +215,7 @@ class AuthTokens(NavienBaseModel):
186
215
 
187
216
  Uses cached expiration time for efficiency.
188
217
  """
189
- return self._expires_at - datetime.now()
218
+ return self._expires_at - datetime.now(UTC)
190
219
 
191
220
  @property
192
221
  def bearer_token(self) -> str:
@@ -66,7 +66,7 @@ async def _detect_unit_system(
66
66
  _logger.warning(
67
67
  "Timed out detecting unit system, defaulting to us_customary"
68
68
  )
69
- return None
69
+ return "us_customary"
70
70
 
71
71
 
72
72
  def async_command(f: Any) -> Any:
@@ -452,14 +452,15 @@ async def handle_set_anti_legionella_period_request(
452
452
  # Get current enabled state
453
453
  use = getattr(status, "anti_legionella_use", None)
454
454
 
455
- # If enabled, keep it enabled; otherwise, enable it
456
- # (period only, no disable-state for set operation)
457
455
  if use:
458
456
  await mqtt.control.enable_anti_legionella(device, period_days)
457
+ print(f"Anti-Legionella period set to {period_days} day(s)")
459
458
  else:
460
- await mqtt.control.enable_anti_legionella(device, period_days)
461
-
462
- print(f" Anti-Legionella period set to {period_days} day(s)")
459
+ print(
460
+ "Anti-Legionella is currently disabled. "
461
+ "Enable it first to set the period, or use "
462
+ "'anti-legionella enable' with the desired period."
463
+ )
463
464
  except (RangeValidationError, ValidationError) as e:
464
465
  _logger.error(f"Failed to set Anti-Legionella period: {e}")
465
466
  except DeviceError as e:
@@ -252,8 +252,8 @@ class EventEmitter:
252
252
 
253
253
  called_count += 1
254
254
 
255
- # Check if this is a once listener using O(1) set lookup
256
- if (event, listener.callback) in self._once_callbacks:
255
+ # Check if this is a once listener
256
+ if listener.once:
257
257
  listeners_to_remove.append(listener)
258
258
  self._once_callbacks.discard((event, listener.callback))
259
259
 
@@ -18,6 +18,8 @@ Example:
18
18
  ... devices = await api.list_devices()
19
19
  """
20
20
 
21
+ import asyncio
22
+
21
23
  from .api_client import NavienAPIClient
22
24
  from .auth import NavienAuthClient
23
25
  from .mqtt import NavienMqttClient
@@ -75,13 +77,20 @@ async def create_navien_clients(
75
77
  # Authenticate and enter context manager
76
78
  try:
77
79
  await auth_client.__aenter__()
78
- except BaseException:
79
- # Ensure session is cleaned up if authentication fails
80
- await auth_client.__aexit__(None, None, None)
80
+ except asyncio.CancelledError:
81
+ # Shield cleanup from further cancellation
82
+ await asyncio.shield(auth_client.__aexit__(None, None, None))
83
+ raise
84
+ except Exception as exc:
85
+ await auth_client.__aexit__(type(exc), exc, exc.__traceback__)
81
86
  raise
82
87
 
83
88
  # Create API and MQTT clients that share the session
84
- api_client = NavienAPIClient(auth_client=auth_client)
85
- mqtt_client = NavienMqttClient(auth_client=auth_client)
89
+ try:
90
+ api_client = NavienAPIClient(auth_client=auth_client)
91
+ mqtt_client = NavienMqttClient(auth_client=auth_client)
92
+ except Exception as exc:
93
+ await auth_client.__aexit__(type(exc), exc, exc.__traceback__)
94
+ raise
86
95
 
87
96
  return auth_client, api_client, mqtt_client
@@ -648,10 +648,13 @@ class MqttDeviceController:
648
648
 
649
649
  @requires_capability("holiday_use")
650
650
  async def set_vacation_days(self, device: Device, days: int) -> int:
651
- """Set vacation/away mode duration (1-365 days)."""
652
- self._validate_range("days", days, 1, 365)
651
+ """Set vacation/away mode duration (1-30 days)."""
652
+ self._validate_range("days", days, 1, 30)
653
653
  return await self._mode_command(
654
- device, CommandCode.GOOUT_DAY, "goout-day", [days]
654
+ device,
655
+ CommandCode.DHW_MODE,
656
+ "dhw-mode",
657
+ [DhwOperationSetting.VACATION.value, days],
655
658
  )
656
659
 
657
660
  @requires_capability("program_reservation_use")
@@ -304,26 +304,42 @@ class MqttSubscriptionManager:
304
304
  self._subscriptions.clear()
305
305
  self._message_handlers.clear()
306
306
 
307
- # Re-establish each subscription
307
+ # Re-establish each subscription — one network call per topic,
308
+ # regardless of how many handlers are registered for it.
308
309
  failed_subscriptions: set[str] = set()
309
310
  for topic, qos in subscriptions_to_restore:
310
311
  handlers = handlers_to_restore.get(topic, [])
311
- for handler in handlers:
312
- try:
313
- await self.subscribe(topic, handler, qos)
314
- except (AwsCrtError, RuntimeError) as e:
315
- _logger.error(
316
- f"Failed to re-subscribe to "
317
- f"'{redact_topic(topic)}': {e}"
318
- )
319
- # Mark topic as failed and skip remaining handlers
320
- # since they will fail for the same reason
321
- failed_subscriptions.add(topic)
322
- break # Exit handler loop, move to next topic
312
+ if not handlers:
313
+ continue
314
+ try:
315
+ # One network subscribe for the first handler
316
+ await self.subscribe(topic, handlers[0], qos)
317
+ except (AwsCrtError, RuntimeError) as e:
318
+ _logger.error(
319
+ f"Failed to re-subscribe to '{redact_topic(topic)}': {e}"
320
+ )
321
+ failed_subscriptions.add(topic)
322
+ continue
323
+
324
+ # Register remaining handlers without extra network calls
325
+ for handler in handlers[1:]:
326
+ if handler not in self._message_handlers[topic]:
327
+ self._message_handlers[topic].append(handler)
323
328
 
324
329
  if failed_subscriptions:
330
+ # Restore failed subscriptions to internal state so they can be
331
+ # retried on the next reconnection cycle.
332
+ qos_map = dict(subscriptions_to_restore)
333
+ for topic in failed_subscriptions:
334
+ self._subscriptions[topic] = qos_map.get(
335
+ topic, mqtt.QoS.AT_LEAST_ONCE
336
+ )
337
+ self._message_handlers[topic] = handlers_to_restore.get(
338
+ topic, []
339
+ )
325
340
  _logger.warning(
326
- f"Failed to restore {len(failed_subscriptions)} subscription(s)"
341
+ f"Failed to restore {len(failed_subscriptions)} "
342
+ "subscription(s); will retry on next reconnection"
327
343
  )
328
344
  else:
329
345
  _logger.info("All subscriptions re-established successfully")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 7.4.8
3
+ Version: 7.4.9
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
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Requires-Python: >=3.13
19
19
  Description-Content-Type: text/x-rst; charset=UTF-8
20
20
  License-File: LICENSE.txt
21
- Requires-Dist: aiohttp>=3.8.0
22
- Requires-Dist: awsiotsdk>=1.27.0
23
- Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: aiohttp>=3.13.5
22
+ Requires-Dist: awsiotsdk>=1.28.2
23
+ Requires-Dist: pydantic>=2.12.5
24
24
  Provides-Extra: cli
25
- Requires-Dist: click>=8.0.0; extra == "cli"
26
- Requires-Dist: rich>=13.0.0; extra == "cli"
25
+ Requires-Dist: click>=8.3.0; extra == "cli"
26
+ Requires-Dist: rich>=14.3.0; extra == "cli"
27
27
  Provides-Extra: testing
28
28
  Requires-Dist: setuptools; extra == "testing"
29
29
  Requires-Dist: pytest; extra == "testing"
@@ -71,6 +71,7 @@ examples/advanced/device_capabilities.py
71
71
  examples/advanced/device_status_debug.py
72
72
  examples/advanced/energy_analytics.py
73
73
  examples/advanced/error_code_demo.py
74
+ examples/advanced/firmware_payload_capture.py
74
75
  examples/advanced/mqtt_diagnostics.py
75
76
  examples/advanced/power_control.py
76
77
  examples/advanced/recirculation_control.py
@@ -1,10 +1,10 @@
1
- aiohttp>=3.8.0
2
- awsiotsdk>=1.27.0
3
- pydantic>=2.0.0
1
+ aiohttp>=3.13.5
2
+ awsiotsdk>=1.28.2
3
+ pydantic>=2.12.5
4
4
 
5
5
  [cli]
6
- click>=8.0.0
7
- rich>=13.0.0
6
+ click>=8.3.0
7
+ rich>=14.3.0
8
8
 
9
9
  [dev]
10
10
  ruff>=0.1.0