nwp500-python 9.0.0__tar.gz → 9.2.1__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 (219) hide show
  1. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/CHANGELOG.rst +168 -0
  2. {nwp500_python-9.0.0/src/nwp500_python.egg-info → nwp500_python-9.2.1}/PKG-INFO +1 -1
  3. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/events.rst +17 -0
  4. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/air_filter_reset.py +2 -1
  5. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/anti_legionella.py +0 -0
  6. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/auto_recovery.py +6 -5
  7. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/combined_callbacks.py +3 -2
  8. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/demand_response.py +2 -1
  9. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/device_capabilities.py +5 -7
  10. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/device_status_debug.py +6 -8
  11. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/energy_analytics.py +4 -3
  12. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/error_code_demo.py +0 -0
  13. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/firmware_payload_capture.py +2 -1
  14. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/mqtt_diagnostics.py +6 -6
  15. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/power_control.py +2 -1
  16. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/recirculation_control.py +2 -1
  17. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/reconnection_demo.py +2 -1
  18. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/reservation_schedule.py +0 -0
  19. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/simple_auto_recovery.py +4 -3
  20. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/token_restoration.py +5 -6
  21. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/tou_schedule.py +1 -1
  22. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/water_reservation.py +2 -1
  23. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/01_authentication.py +3 -2
  24. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/02_list_devices.py +11 -8
  25. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/04_set_temperature.py +2 -1
  26. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/advanced_auth_patterns.py +2 -1
  27. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/command_queue.py +2 -1
  28. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/device_status_callback.py +5 -7
  29. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/event_driven_control.py +4 -3
  30. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/mqtt_realtime_monitoring.py +6 -8
  31. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/periodic_requests.py +5 -4
  32. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/set_mode.py +2 -1
  33. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/vacation_mode.py +2 -1
  34. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/mask.py +5 -2
  35. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/periodic_device_info.py +5 -4
  36. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/simple_periodic_info.py +0 -0
  37. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_api_client.py +8 -8
  38. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_connection.py +2 -1
  39. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_messaging.py +16 -13
  40. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_periodic_minimal.py +13 -10
  41. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/bump_version.py +86 -16
  42. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/__init__.py +9 -0
  43. nwp500_python-9.2.1/src/nwp500/cli/output_formatters.py +172 -0
  44. nwp500_python-9.0.0/src/nwp500/cli/output_formatters.py → nwp500_python-9.2.1/src/nwp500/cli/presentation.py +189 -358
  45. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/rich_output.py +144 -402
  46. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/device_capabilities.py +2 -0
  47. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/encoding.py +2 -2
  48. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/events.py +20 -3
  49. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/feature.py +20 -2
  50. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/schedule.py +51 -0
  51. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/status.py +20 -2
  52. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/tou.py +43 -0
  53. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/__init__.py +3 -0
  54. nwp500_python-9.2.1/src/nwp500/mqtt/_control_commands.py +217 -0
  55. nwp500_python-9.2.1/src/nwp500/mqtt/_device_subscriptions.py +227 -0
  56. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/client.py +22 -401
  57. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/command_queue.py +2 -5
  58. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/connection.py +49 -9
  59. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/control.py +32 -2
  60. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/state_tracker.py +8 -7
  61. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/subscriptions.py +80 -11
  62. nwp500_python-9.2.1/src/nwp500/mqtt/types.py +50 -0
  63. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/utils.py +2 -3
  64. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt_events.py +14 -2
  65. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/reservations.py +77 -2
  66. nwp500_python-9.2.1/src/nwp500/tou_schedule.py +105 -0
  67. {nwp500_python-9.0.0 → nwp500_python-9.2.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  68. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/SOURCES.txt +9 -0
  69. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/scm_file_list.json +182 -173
  70. nwp500_python-9.2.1/src/nwp500_python.egg-info/scm_version.json +8 -0
  71. nwp500_python-9.2.1/tests/test_canonical_schedule.py +161 -0
  72. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_command_queue.py +3 -4
  73. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_device_capabilities.py +31 -1
  74. nwp500_python-9.2.1/tests/test_mqtt_events.py +120 -0
  75. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reliability.py +86 -2
  76. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_protocol_correctness.py +133 -9
  77. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_reservations.py +132 -0
  78. nwp500_python-9.2.1/tests/test_tou_schedule.py +192 -0
  79. nwp500_python-9.2.1/tests/test_unit_system_process_wide.py +133 -0
  80. nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  81. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.coveragerc +0 -0
  82. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  83. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/copilot-instructions.md +0 -0
  84. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/workflows/ci.yml +0 -0
  85. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/workflows/release.yml +0 -0
  86. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.gitignore +0 -0
  87. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.pre-commit-config.yaml +0 -0
  88. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.readthedocs.yml +0 -0
  89. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/AUTHORS.rst +0 -0
  90. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/CONTRIBUTING.rst +0 -0
  91. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/LICENSE.txt +0 -0
  92. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/Makefile +0 -0
  93. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/README.rst +0 -0
  94. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/RELEASE.md +0 -0
  95. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/Makefile +0 -0
  96. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/_static/.gitignore +0 -0
  97. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/conf.py +0 -0
  98. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/advanced-features.rst +0 -0
  99. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/architecture.rst +0 -0
  100. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/index.rst +0 -0
  101. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/authenticate.rst +0 -0
  102. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/auto-recovery.rst +0 -0
  103. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/diagnose-mqtt.rst +0 -0
  104. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/home-assistant.rst +0 -0
  105. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/index.rst +0 -0
  106. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/maintenance.rst +0 -0
  107. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/manage-units.rst +0 -0
  108. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/monitor-status.rst +0 -0
  109. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/optimize-tou.rst +0 -0
  110. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/queue-commands.rst +0 -0
  111. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/schedule-operation.rst +0 -0
  112. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/track-energy.rst +0 -0
  113. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/index.rst +0 -0
  114. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/openapi.yaml +0 -0
  115. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/authors.rst +0 -0
  116. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/changelog.rst +0 -0
  117. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/contributing.rst +0 -0
  118. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/history.rst +0 -0
  119. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/license.rst +0 -0
  120. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/configuration.rst +0 -0
  121. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/enumerations.rst +0 -0
  122. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/index.rst +0 -0
  123. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/installation.rst +0 -0
  124. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/data_conversions.rst +0 -0
  125. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_features.rst +0 -0
  126. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_status.rst +0 -0
  127. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/error_codes.rst +0 -0
  128. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  129. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/quick_reference.rst +0 -0
  130. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/rest_api.rst +0 -0
  131. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/api_client.rst +0 -0
  132. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/auth_client.rst +0 -0
  133. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/cli.rst +0 -0
  134. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/exceptions.rst +0 -0
  135. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/models.rst +0 -0
  136. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/mqtt_client.rst +0 -0
  137. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/requirements.txt +0 -0
  138. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/tutorials/getting-started.rst +0 -0
  139. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/.ruff.toml +0 -0
  140. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/README.md +0 -0
  141. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/tou_openei.py +0 -0
  142. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/03_get_status.py +0 -0
  143. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/error_handling.py +0 -0
  144. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/improved_auth.py +0 -0
  145. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
  146. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/pyproject.toml +0 -0
  147. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/README.md +0 -0
  148. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/diagnose_mqtt_connection.py +0 -0
  149. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/extract_changelog.py +0 -0
  150. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/format.py +0 -0
  151. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/lint.py +0 -0
  152. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/setup-dev.py +0 -0
  153. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/validate_version.py +0 -0
  154. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/setup.cfg +0 -0
  155. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/setup.py +0 -0
  156. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/_base.py +0 -0
  157. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/api_client.py +0 -0
  158. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/auth.py +0 -0
  159. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/__init__.py +0 -0
  160. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/__main__.py +0 -0
  161. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/handlers.py +0 -0
  162. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/monitoring.py +0 -0
  163. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/token_storage.py +0 -0
  164. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/command_decorators.py +0 -0
  165. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/config.py +0 -0
  166. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/converters.py +0 -0
  167. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/device_info_cache.py +0 -0
  168. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/enums.py +0 -0
  169. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/exceptions.py +0 -0
  170. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/factory.py +0 -0
  171. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/field_factory.py +0 -0
  172. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/__init__.py +0 -0
  173. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/_converters.py +0 -0
  174. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/device.py +0 -0
  175. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/energy.py +0 -0
  176. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/mqtt_models.py +0 -0
  177. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/diagnostics.py +0 -0
  178. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/periodic.py +0 -0
  179. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/reconnection.py +0 -0
  180. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/openei.py +0 -0
  181. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/py.typed +0 -0
  182. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/temperature.py +0 -0
  183. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/topic_builder.py +0 -0
  184. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/unit_system.py +0 -0
  185. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/utils.py +0 -0
  186. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  187. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  188. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  189. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/requires.txt +0 -0
  190. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
  191. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/conftest.py +0 -0
  192. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_api_helpers.py +0 -0
  193. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_auth.py +0 -0
  194. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_auth_session_lifecycle.py +0 -0
  195. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_bug_fixes.py +0 -0
  196. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_cli_basic.py +0 -0
  197. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_cli_commands.py +0 -0
  198. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_command_decorators.py +0 -0
  199. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_device_info_cache.py +0 -0
  200. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_events.py +0 -0
  201. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_exceptions.py +0 -0
  202. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_model_converters.py +0 -0
  203. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_models.py +0 -0
  204. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_clean_session_resume.py +0 -0
  205. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_client_init.py +0 -0
  206. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_hypothesis.py +0 -0
  207. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection.py +0 -0
  208. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection_storm.py +0 -0
  209. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_multi_device.py +0 -0
  210. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_openei.py +0 -0
  211. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_public_api.py +0 -0
  212. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_temperature_converters.py +0 -0
  213. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_threading_model.py +0 -0
  214. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_token_storage.py +0 -0
  215. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_tou_api.py +0 -0
  216. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_unit_switching.py +0 -0
  217. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_utility_modules.py +0 -0
  218. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_utils.py +0 -0
  219. {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tox.ini +0 -0
@@ -5,6 +5,174 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.2.1 (2026-07-30)
9
+ ==========================
10
+
11
+ Added
12
+ -----
13
+ - **Confirmed reservation/TOU write helpers and canonical schedule
14
+ comparison** (`#111 <https://github.com/eman/nwp500-python/issues/111>`_):
15
+ ``update_reservations`` and ``configure_tou_schedule`` were
16
+ fire-and-forget, returning only an MQTT publish packet id with no way to
17
+ confirm the device applied the write. New
18
+ ``nwp500.reservations.update_reservations_confirmed()`` and
19
+ ``nwp500.tou_schedule.configure_tou_schedule_confirmed()`` (also exported
20
+ from the top-level ``nwp500`` package) send the write and await the
21
+ device's ``rsv/rd``/``tou/rd`` echo, returning the parsed schedule the
22
+ device now holds (or ``None`` on timeout). Because the protocol has no
23
+ request/response correlation id on these topics, a response is only
24
+ accepted once it matches the content that was just written, so a
25
+ stale/unrelated response from a concurrent read or a previous write
26
+ can't be mistaken for confirmation. Also added
27
+ ``ReservationEntry.canonical_key()``/``ReservationSchedule.canonical()``
28
+ and the ``TOUPeriod``/``TOUReservationSchedule`` equivalents: a stable,
29
+ order-independent, hashable representation of the raw protocol fields so
30
+ a desired program can be compared against a device read-back with
31
+ ``desired.canonical() == confirmed.canonical()`` instead of hand-diffing.
32
+
33
+ Fixed
34
+ -----
35
+ - **Freeze protection temperature now validated against device limits**
36
+ (`#112 <https://github.com/eman/nwp500-python/issues/112>`_):
37
+ ``set_freeze_protection_temperature`` documented a 35-45°F valid range
38
+ but sent the value straight through with no validation. It now fetches
39
+ device features and validates against
40
+ ``freeze_protection_temp_min``/``freeze_protection_temp_max`` (raising
41
+ ``RangeValidationError`` if out of range), matching the pattern already
42
+ used by ``set_dhw_temperature``. The command is also now gated behind a
43
+ new ``freeze_protection_use`` capability (added to
44
+ ``MqttDeviceCapabilityChecker._CAPABILITY_MAP`` and enforced with
45
+ ``@requires_capability``), so it's no longer dispatched to devices that
46
+ don't support freeze protection at all.
47
+ - **Demand response commands gated on the dr_setting_use capability**
48
+ (`#114 <https://github.com/eman/nwp500-python/issues/114>`_):
49
+ ``enable_demand_response``/``disable_demand_response`` were dispatched
50
+ unconditionally even though the ``dr_setting_use`` capability flag
51
+ exists on ``DeviceFeature``. Every other controllable feature follows
52
+ flag + ``_CAPABILITY_MAP`` entry + ``@requires_capability`` decorator;
53
+ DR was the one gap. Added the missing map entry and decorators so DR
54
+ now follows the same pattern.
55
+
56
+ Documentation
57
+ -------------
58
+ - **decode_reservation_hex docstring corrected** (`#113
59
+ <https://github.com/eman/nwp500-python/issues/113>`_): the ``param``
60
+ byte was documented as "temperature offset by 20°F", contradicting the
61
+ half-degrees-Celsius encoding used consistently everywhere else
62
+ (``models/schedule.py``, ``models/_converters.py``). Corrected to
63
+ "target temperature, half-degrees Celsius; °C = param/2", and fixed the
64
+ accompanying doctest-style example, which showed a ``minute`` key where
65
+ the function actually returns ``min``.
66
+
67
+ Version 9.2.0 (2026-07-06)
68
+ ==========================
69
+
70
+ Changed
71
+ -------
72
+ - **CLI formatting stacks merged; Rich is the sole human renderer**
73
+ (`#100 <https://github.com/eman/nwp500-python/issues/100>`_): a new
74
+ ``src/nwp500/cli/presentation.py`` owns all data-shaping for CLI output
75
+ (field selection, labels, units, ordering, value formatting and energy
76
+ aggregation) as presentation-neutral structures. Because the CLI
77
+ hard-requires ``rich`` (there is no plain-text fallback), the redundant
78
+ plain-text human renderer and the Rich-vs-plain fallback machinery were
79
+ removed: ``rich_output.py`` no longer contains ``_should_use_rich``,
80
+ ``_rich_available``, the ``NWP500_NO_RICH`` toggle, or any
81
+ ``_print_*_plain`` methods, and now renders the neutral structures
82
+ (including the energy ``TOTAL SUMMARY``) exclusively with Rich, consuming
83
+ the presentation dataclasses directly instead of dict adapters.
84
+ ``output_formatters.py`` keeps only JSON/CSV rendering plus thin
85
+ human-output dispatch. Net CLI code drops from ~2088 to ~1838 lines with a
86
+ single data-shaping layer and a single human renderer. Non-energy output
87
+ is byte-for-byte unchanged (verified by golden capture and the existing
88
+ CLI tests); energy output now renders a summary table plus a breakdown
89
+ table entirely in Rich rather than printing plain text and a Rich table.
90
+ - **mqtt/client.py slimmed toward a thin façade** (`#99
91
+ <https://github.com/eman/nwp500-python/issues/99>`_): the ~40 device
92
+ control command proxies and the typed ``subscribe_*``/``unsubscribe_*``
93
+ device-subscription proxies were moved out of ``NavienMqttClient`` into
94
+ two focused mixins, ``DeviceControlCommandsMixin``
95
+ (``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
96
+ (``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
97
+ both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
98
+ from ~1572 to ~1196 lines and reads more clearly as connection
99
+ orchestration plus a public façade. No behavior change.
100
+ - **Event system relationship clarified and de-duplicated** (`#102
101
+ <https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
102
+ ``mqtt_events.py`` are not two competing event mechanisms.
103
+ ``EventEmitter`` (``events.py``) is the sole delivery mechanism, while
104
+ ``mqtt_events.py`` provides the event-name registry
105
+ (``MqttClientEvents``) and the typed dataclass payloads it carries.
106
+ Internal ``emit`` call sites in ``mqtt/state_tracker.py``,
107
+ ``mqtt/client.py`` and ``mqtt/subscriptions.py`` now reference the
108
+ ``MqttClientEvents`` constants instead of duplicating the raw event-name
109
+ strings, so each event name is defined in exactly one place. Module
110
+ docstrings and the event-system reference docs were updated to document
111
+ the relationship, and typed-payload delivery is now covered by tests.
112
+ - **awscrt types wrapped out of public MQTT signatures** (`#101
113
+ <https://github.com/eman/nwp500-python/issues/101>`_): the library no
114
+ longer exposes ``awscrt`` SDK types on its own public/semi-public API
115
+ surface. A library-owned ``nwp500.mqtt.QoS`` ``IntEnum`` (also exported
116
+ as ``nwp500.QoS``) now replaces ``awscrt.mqtt.QoS`` on the ``publish``
117
+ and ``subscribe`` methods of ``NavienMqttClient``, ``MqttConnection``
118
+ and ``MqttSubscriptionManager``, on ``MqttCommandQueue.enqueue`` and on
119
+ the ``QueuedCommand`` dataclass. ``awscrt.mqtt.Connection`` handles are
120
+ typed behind the ``MqttConnectionHandle`` alias, and translation to/from
121
+ ``awscrt`` happens only at the connection-layer boundary
122
+ (``nwp500/mqtt/types.py``). ``NavienMqttClient.publish`` now wraps
123
+ otherwise-uncaught ``AwsCrtError`` in ``MqttPublishError`` so ``awscrt``
124
+ exceptions no longer leak out of the public boundary.
125
+
126
+ .. code-block:: python
127
+
128
+ # OLD
129
+ from awscrt import mqtt
130
+ await client.publish(topic, payload, qos=mqtt.QoS.AT_LEAST_ONCE)
131
+
132
+ # NEW
133
+ from nwp500 import QoS
134
+ await client.publish(topic, payload, qos=QoS.AT_LEAST_ONCE)
135
+
136
+ Fixed
137
+ -----
138
+ - **MQTT ack futures now consumed on abandonment** (`#97
139
+ <https://github.com/eman/nwp500-python/issues/97>`_): ``_await_ack()``
140
+ in ``mqtt/connection.py`` and the inline subscribe/unsubscribe
141
+ acknowledgement waits in ``mqtt/subscriptions.py`` shield the AWS CRT
142
+ future so a timeout or cancellation doesn't propagate into the SDK
143
+ future. Previously, if the shielded future later completed with an
144
+ exception (e.g. ``AwsCrtError`` from clean-session cancellation during
145
+ reconnect) after the awaiting task had already given up, nobody
146
+ retrieved that exception, and asyncio logged a "Future exception was
147
+ never retrieved" warning at garbage-collection time. A done callback
148
+ is now attached whenever a wait is abandoned so the eventual
149
+ result/exception is always retrieved and logged at debug level
150
+ instead of leaking as an unhandled asyncio warning.
151
+
152
+ Documentation
153
+ -------------
154
+ - **Unit-system preference is a deliberate process-wide global** (`#103
155
+ <https://github.com/eman/nwp500-python/issues/103>`_): clarified and
156
+ locked in that the preference in ``nwp500.unit_system`` is an intentional
157
+ process-wide module-level global rather than a
158
+ ``contextvars.ContextVar``. A context-local value silently reverted to
159
+ auto-detect for real-time data because MQTT message handling runs in tasks
160
+ scheduled from AWS CRT callback threads whose context never inherits from
161
+ the application task. Added prominent docstring notes to the affected
162
+ models (``DeviceStatus``, ``DeviceFeature``, ``ReservationEntry``,
163
+ ``WeeklyReservationEntry``, and ``TOUPeriod``) explaining that unit-aware
164
+ computed fields read the preference at access time and share it across all
165
+ async tasks and threads. This is a non-breaking documentation and test
166
+ change; no API changes.
167
+
168
+ Added
169
+ -----
170
+ - **Tests for process-wide unit-system semantics** (`#103
171
+ <https://github.com/eman/nwp500-python/issues/103>`_): new
172
+ ``tests/test_unit_system_process_wide.py`` verifies that setting the global
173
+ preference affects already-constructed model instances at access time and
174
+ that the preference is visible across async tasks and threads.
175
+
8
176
  Version 9.0.0 (2026-07-05)
9
177
  ==========================
10
178
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.0.0
3
+ Version: 9.2.1
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
@@ -17,6 +17,23 @@ Use the event system when you want to react to connection changes, status
17
17
  transitions, or derived state changes such as temperature deltas and error
18
18
  conditions.
19
19
 
20
+ How the pieces fit together
21
+ ---------------------------
22
+
23
+ The event system is split across two complementary modules:
24
+
25
+ * :mod:`nwp500.events` provides the delivery **mechanism** — the generic
26
+ :class:`~nwp500.events.EventEmitter` (multiple listeners, async handlers,
27
+ one-time listeners, priority ordering). ``NavienMqttClient`` extends it.
28
+ * :mod:`nwp500.mqtt_events` provides the **vocabulary** — the
29
+ :class:`~nwp500.mqtt_events.MqttClientEvents` name registry and the typed,
30
+ frozen dataclass payloads carried by each event.
31
+
32
+ These are not two competing systems. Internal ``emit`` call sites reference the
33
+ ``MqttClientEvents`` constants and emit the matching payload dataclass, and you
34
+ subscribe with the same constants, so an event name is defined in exactly one
35
+ place.
36
+
20
37
  Two Subscription Patterns
21
38
  =========================
22
39
 
@@ -8,7 +8,8 @@ after cleaning or replacing the filter on heat pump models.
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient
11
+
12
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
12
13
 
13
14
  # Set up logging to see the air filter control process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
21
21
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
22
22
 
23
23
  from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
24
+ from nwp500.exceptions import Nwp500Error
24
25
  from nwp500.mqtt import MqttConnectionConfig
25
26
 
26
27
  # Configure logging
@@ -167,7 +168,7 @@ async def strategy_full_recreation(auth_client, device):
167
168
  mqtt_client = await create_and_connect()
168
169
  recovery_attempt = 0 # Reset on success
169
170
  logger.info("Successfully recreated MQTT client")
170
- except Exception as e:
171
+ except Nwp500Error as e:
171
172
  logger.error(f"Failed to recreate MQTT client: {e}")
172
173
 
173
174
  try:
@@ -250,7 +251,7 @@ async def strategy_token_refresh(auth_client, device):
250
251
 
251
252
  recovery_attempt = 0 # Reset on success
252
253
 
253
- except Exception as e:
254
+ except Nwp500Error as e:
254
255
  logger.error(f"Failed to refresh and reconnect: {e}")
255
256
 
256
257
  mqtt_client = NavienMqttClient(auth_client, config=config)
@@ -348,7 +349,7 @@ async def strategy_exponential_backoff(auth_client, device):
348
349
  try:
349
350
  if mqtt_client.is_connected:
350
351
  await mqtt_client.disconnect()
351
- except Exception as e:
352
+ except Nwp500Error as e:
352
353
  logger.warning(f"Error disconnecting old client: {e}")
353
354
 
354
355
  # Create new client
@@ -365,7 +366,7 @@ async def strategy_exponential_backoff(auth_client, device):
365
366
  recovery_attempt = 0 # Reset on success
366
367
  logger.info("All subscriptions restored")
367
368
 
368
- except Exception as e:
369
+ except Nwp500Error as e:
369
370
  logger.error(f"Recovery attempt failed: {e}")
370
371
 
371
372
  mqtt_client = NavienMqttClient(auth_client, config=config)
@@ -442,7 +443,7 @@ if __name__ == "__main__":
442
443
  asyncio.run(main())
443
444
  except KeyboardInterrupt:
444
445
  print("\n\n[WARNING] Interrupted by user")
445
- except Exception as e:
446
+ except Nwp500Error as e:
446
447
  print(f"\n[ERROR] Error: {e}")
447
448
  import traceback
448
449
 
@@ -32,6 +32,7 @@ if __name__ == "__main__":
32
32
  from nwp500.api_client import NavienAPIClient
33
33
  from nwp500.auth import NavienAuthClient
34
34
  from nwp500.enums import OnOffFlag
35
+ from nwp500.exceptions import Nwp500Error
35
36
  from nwp500.models import DeviceFeature, DeviceStatus
36
37
  from nwp500.mqtt import NavienMqttClient
37
38
 
@@ -157,7 +158,7 @@ async def main():
157
158
  await mqtt_client.disconnect()
158
159
  print("\n[SUCCESS] Disconnected")
159
160
 
160
- except Exception as e:
161
+ except Nwp500Error as e:
161
162
  print(f"[ERROR] Error: {e}")
162
163
  if mqtt_client.is_connected:
163
164
  await mqtt_client.disconnect()
@@ -165,7 +166,7 @@ async def main():
165
166
 
166
167
  return 0
167
168
 
168
- except Exception as e:
169
+ except Nwp500Error as e:
169
170
  print(f"[ERROR] Error: {e}")
170
171
  import traceback
171
172
 
@@ -8,7 +8,8 @@ to help utilities manage peak loads.
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient
11
+
12
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
12
13
 
13
14
  # Set up logging to see the demand response control process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -26,6 +26,8 @@ logging.getLogger("nwp500.mqtt_client").setLevel(logging.INFO)
26
26
  logging.getLogger("nwp500.auth").setLevel(logging.INFO)
27
27
  logging.getLogger("nwp500.api_client").setLevel(logging.INFO)
28
28
 
29
+ logger = logging.getLogger(__name__)
30
+
29
31
  # If running from examples directory, add parent to path
30
32
  if __name__ == "__main__":
31
33
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
@@ -39,7 +41,7 @@ from nwp500.mqtt import NavienMqttClient
39
41
 
40
42
  try:
41
43
  from mask import mask_mac # type: ignore
42
- except Exception:
44
+ except ImportError:
43
45
 
44
46
  def mask_mac(mac): # pragma: no cover - fallback
45
47
  return "[REDACTED_MAC]"
@@ -254,9 +256,7 @@ async def main():
254
256
  print("[SUCCESS] Disconnected successfully")
255
257
 
256
258
  except Exception:
257
- import logging
258
-
259
- logging.exception("MQTT error in device_feature_callback")
259
+ logger.exception("MQTT error in device_feature_callback")
260
260
 
261
261
  if mqtt_client.is_connected:
262
262
  await mqtt_client.disconnect()
@@ -276,9 +276,7 @@ async def main():
276
276
  return 1
277
277
 
278
278
  except Exception:
279
- import logging
280
-
281
- logging.exception("Unexpected error in device_feature_callback")
279
+ logger.exception("Unexpected error in device_feature_callback")
282
280
  return 1
283
281
 
284
282
 
@@ -18,6 +18,8 @@ logging.basicConfig(
18
18
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19
19
  )
20
20
 
21
+ logger = logging.getLogger(__name__)
22
+
21
23
  # If running from examples directory, add parent to path
22
24
  if __name__ == "__main__":
23
25
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
@@ -31,7 +33,7 @@ from nwp500.mqtt import NavienMqttClient
31
33
 
32
34
  try:
33
35
  from mask import mask_mac # type: ignore
34
- except Exception:
36
+ except ImportError:
35
37
 
36
38
  def mask_mac(mac): # pragma: no cover - fallback
37
39
  return "[REDACTED_MAC]"
@@ -79,7 +81,7 @@ async def main():
79
81
 
80
82
  try:
81
83
  from mask import mask_any # type: ignore
82
- except Exception:
84
+ except ImportError:
83
85
 
84
86
  def mask_any(_):
85
87
  return "[REDACTED]"
@@ -177,9 +179,7 @@ async def main():
177
179
  print("[SUCCESS] Disconnected successfully")
178
180
 
179
181
  except Exception:
180
- import logging
181
-
182
- logging.exception("MQTT error in device_status_callback_debug")
182
+ logger.exception("MQTT error in device_status_callback_debug")
183
183
 
184
184
  if mqtt_client.is_connected:
185
185
  await mqtt_client.disconnect()
@@ -199,9 +199,7 @@ async def main():
199
199
  return 1
200
200
 
201
201
  except Exception:
202
- import logging
203
-
204
- logging.exception("Unexpected error in device_status_callback_debug")
202
+ logger.exception("Unexpected error in device_status_callback_debug")
205
203
  return 1
206
204
 
207
205
 
@@ -15,7 +15,7 @@ The energy data comes from the EMS (Energy Management System) API.
15
15
  import asyncio
16
16
  import os
17
17
  import sys
18
- from datetime import datetime
18
+ from datetime import UTC, datetime
19
19
 
20
20
  from nwp500 import (
21
21
  EnergyUsageResponse,
@@ -23,6 +23,7 @@ from nwp500 import (
23
23
  NavienAuthClient,
24
24
  NavienMqttClient,
25
25
  )
26
+ from nwp500.exceptions import Nwp500Error
26
27
 
27
28
 
28
29
  async def main():
@@ -135,7 +136,7 @@ async def main():
135
136
  print("[OK] Subscribed to energy usage responses")
136
137
 
137
138
  # Request energy usage for current month
138
- now = datetime.now()
139
+ now = datetime.now(UTC).astimezone()
139
140
  current_year = now.year
140
141
  current_month = now.month
141
142
 
@@ -160,7 +161,7 @@ if __name__ == "__main__":
160
161
  asyncio.run(main())
161
162
  except KeyboardInterrupt:
162
163
  print("\n\nInterrupted by user")
163
- except Exception as e:
164
+ except Nwp500Error as e:
164
165
  print(f"\n\nError: {e}")
165
166
  import traceback
166
167
 
@@ -37,6 +37,7 @@ from pathlib import Path
37
37
  from typing import Any
38
38
 
39
39
  from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
40
+ from nwp500.exceptions import Nwp500Error
40
41
  from nwp500.models import DeviceFeature
41
42
  from nwp500.mqtt.utils import redact, redact_topic
42
43
  from nwp500.topic_builder import MqttTopicBuilder
@@ -163,7 +164,7 @@ async def main() -> None:
163
164
  try:
164
165
  await mqtt_client.request_tou_settings(device, serial)
165
166
  await asyncio.sleep(5)
166
- except Exception as exc:
167
+ except Nwp500Error as exc:
167
168
  print(f" TOU request failed: {exc}")
168
169
 
169
170
  # --- Step 5: wait a bit more to catch any late-arriving messages ---
@@ -94,8 +94,8 @@ class MqttDiagnosticsExample:
94
94
 
95
95
  except asyncio.CancelledError:
96
96
  break
97
- except Exception as e:
98
- _logger.error(f"Error exporting diagnostics: {e}", exc_info=True)
97
+ except Exception:
98
+ _logger.exception("Error exporting diagnostics")
99
99
 
100
100
  async def monitor_connection_state(self, interval: float = 10.0) -> None:
101
101
  """
@@ -124,8 +124,8 @@ class MqttDiagnosticsExample:
124
124
 
125
125
  except asyncio.CancelledError:
126
126
  break
127
- except Exception as e:
128
- _logger.error(f"Error monitoring state: {e}", exc_info=True)
127
+ except Exception:
128
+ _logger.exception("Error monitoring state")
129
129
 
130
130
  async def on_connection_drop(self, event) -> None:
131
131
  """Handle connection drop event."""
@@ -289,8 +289,8 @@ class MqttDiagnosticsExample:
289
289
 
290
290
  _logger.info("Example complete")
291
291
 
292
- except Exception as e:
293
- _logger.error(f"Error during example: {e}", exc_info=True)
292
+ except Exception:
293
+ _logger.exception("Error during example")
294
294
  raise
295
295
 
296
296
 
@@ -8,7 +8,8 @@ and receive confirmation of the power state change.
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient
11
+
12
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
12
13
 
13
14
  # Set up logging to see the power control process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -8,7 +8,8 @@ trigger the hot button, and configure scheduling.
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient
11
+
12
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
12
13
 
13
14
  # Set up logging to see the recirculation control process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
21
21
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
22
22
 
23
23
  from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
24
+ from nwp500.exceptions import Nwp500Error
24
25
  from nwp500.mqtt import MqttConnectionConfig
25
26
 
26
27
 
@@ -142,7 +143,7 @@ if __name__ == "__main__":
142
143
  asyncio.run(main())
143
144
  except KeyboardInterrupt:
144
145
  print("\n\n[WARNING] Interrupted by user")
145
- except Exception as e:
146
+ except Nwp500Error as e:
146
147
  print(f"\n[ERROR] Error: {e}")
147
148
  import traceback
148
149
 
@@ -24,6 +24,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
24
24
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
25
25
 
26
26
  from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
27
+ from nwp500.exceptions import Nwp500Error
27
28
  from nwp500.mqtt import MqttConnectionConfig
28
29
 
29
30
  # Configure logging
@@ -76,7 +77,7 @@ class ResilientMqttClient:
76
77
  if self.mqtt_client and self.mqtt_client.is_connected:
77
78
  try:
78
79
  await self.mqtt_client.disconnect()
79
- except Exception as e:
80
+ except Nwp500Error as e:
80
81
  logger.warning(f"Error disconnecting old client: {e}")
81
82
 
82
83
  # Create new client
@@ -150,7 +151,7 @@ class ResilientMqttClient:
150
151
  self.recovery_attempt = 0
151
152
  logger.info("[SUCCESS] Recovery successful!")
152
153
 
153
- except Exception as e:
154
+ except Nwp500Error as e:
154
155
  logger.error(f"Recovery attempt failed: {e}")
155
156
  # The next reconnection_failed event will trigger another recovery attempt
156
157
  finally:
@@ -252,7 +253,7 @@ if __name__ == "__main__":
252
253
  asyncio.run(main())
253
254
  except KeyboardInterrupt:
254
255
  print("\n\n[WARNING] Interrupted by user")
255
- except Exception as e:
256
+ except Nwp500Error as e:
256
257
  print(f"\n[ERROR] Error: {e}")
257
258
  import traceback
258
259
 
@@ -57,8 +57,7 @@ async def save_tokens_example():
57
57
  token_data = tokens.to_dict()
58
58
 
59
59
  # Save to file
60
- with open(TOKEN_FILE, "w") as f:
61
- json.dump(token_data, f, indent=2)
60
+ await asyncio.to_thread(TOKEN_FILE.write_text, json.dumps(token_data, indent=2))
62
61
 
63
62
  logger.info(f"[OK] Tokens saved to {TOKEN_FILE}")
64
63
  logger.info("You can now use --restore to skip authentication on future runs")
@@ -81,8 +80,7 @@ async def restore_tokens_example():
81
80
  )
82
81
 
83
82
  # Load saved tokens
84
- with open(TOKEN_FILE) as f:
85
- token_data = json.load(f)
83
+ token_data = json.loads(await asyncio.to_thread(TOKEN_FILE.read_text))
86
84
 
87
85
  logger.info(f"Loading tokens from {TOKEN_FILE}...")
88
86
 
@@ -116,8 +114,9 @@ async def restore_tokens_example():
116
114
  if tokens.issued_at != stored_tokens.issued_at:
117
115
  logger.info("Tokens were refreshed, updating stored copy...")
118
116
  token_data = tokens.to_dict()
119
- with open(TOKEN_FILE, "w") as f:
120
- json.dump(token_data, f, indent=2)
117
+ await asyncio.to_thread(
118
+ TOKEN_FILE.write_text, json.dumps(token_data, indent=2)
119
+ )
121
120
  logger.info(f"[OK] Updated tokens saved to {TOKEN_FILE}")
122
121
 
123
122
 
@@ -7,7 +7,7 @@ import sys
7
7
  from typing import Any
8
8
 
9
9
  from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
10
- from nwp500.encoding import decode_week_bitfield, decode_price, build_tou_period
10
+ from nwp500.encoding import build_tou_period, decode_price, decode_week_bitfield
11
11
 
12
12
 
13
13
  async def _wait_for_controller_serial(mqtt_client: NavienMqttClient, device) -> str:
@@ -8,7 +8,8 @@ system for scheduling water heating.
8
8
 
9
9
  import asyncio
10
10
  import logging
11
- from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient
11
+
12
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
12
13
 
13
14
  # Set up logging to see the water program configuration process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -26,6 +26,7 @@ from nwp500.auth import NavienAuthClient
26
26
  from nwp500.exceptions import (
27
27
  AuthenticationError,
28
28
  InvalidCredentialsError,
29
+ Nwp500Error,
29
30
  )
30
31
 
31
32
 
@@ -97,8 +98,8 @@ async def main():
97
98
  print(f"Error code: {e.code}")
98
99
  return 1
99
100
 
100
- except Exception as e:
101
- print(f"\n[ERROR] Unexpected error: {str(e)}")
101
+ except Nwp500Error as e:
102
+ print(f"\n[ERROR] Unexpected error: {e!s}")
102
103
  import traceback
103
104
 
104
105
  traceback.print_exc()