nwp500-python 9.2.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.2.0 → nwp500_python-9.2.1}/CHANGELOG.rst +59 -0
  2. {nwp500_python-9.2.0/src/nwp500_python.egg-info → nwp500_python-9.2.1}/PKG-INFO +1 -1
  3. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/air_filter_reset.py +2 -1
  4. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/anti_legionella.py +0 -0
  5. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/auto_recovery.py +6 -5
  6. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/combined_callbacks.py +3 -2
  7. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/demand_response.py +2 -1
  8. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/device_capabilities.py +5 -7
  9. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/device_status_debug.py +6 -8
  10. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/energy_analytics.py +4 -3
  11. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/error_code_demo.py +0 -0
  12. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/firmware_payload_capture.py +2 -1
  13. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/mqtt_diagnostics.py +6 -6
  14. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/power_control.py +2 -1
  15. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/recirculation_control.py +2 -1
  16. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/reconnection_demo.py +2 -1
  17. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/reservation_schedule.py +0 -0
  18. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/simple_auto_recovery.py +4 -3
  19. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/token_restoration.py +5 -6
  20. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/tou_schedule.py +1 -1
  21. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/water_reservation.py +2 -1
  22. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/01_authentication.py +3 -2
  23. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/02_list_devices.py +11 -8
  24. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/04_set_temperature.py +2 -1
  25. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/advanced_auth_patterns.py +2 -1
  26. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/command_queue.py +2 -1
  27. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/device_status_callback.py +5 -7
  28. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/event_driven_control.py +4 -3
  29. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/mqtt_realtime_monitoring.py +6 -8
  30. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/periodic_requests.py +5 -4
  31. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/set_mode.py +2 -1
  32. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/vacation_mode.py +2 -1
  33. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/mask.py +5 -2
  34. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/periodic_device_info.py +5 -4
  35. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/simple_periodic_info.py +0 -0
  36. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_api_client.py +8 -8
  37. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_connection.py +2 -1
  38. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_messaging.py +16 -13
  39. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_periodic_minimal.py +13 -10
  40. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/__init__.py +7 -0
  41. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/device_capabilities.py +2 -0
  42. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/encoding.py +2 -2
  43. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/schedule.py +31 -0
  44. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/tou.py +35 -0
  45. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/control.py +32 -2
  46. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/reservations.py +77 -2
  47. nwp500_python-9.2.1/src/nwp500/tou_schedule.py +105 -0
  48. {nwp500_python-9.2.0 → nwp500_python-9.2.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  49. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
  50. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/scm_file_list.json +182 -179
  51. nwp500_python-9.2.1/src/nwp500_python.egg-info/scm_version.json +8 -0
  52. nwp500_python-9.2.1/tests/test_canonical_schedule.py +161 -0
  53. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_device_capabilities.py +31 -1
  54. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_protocol_correctness.py +126 -0
  55. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_reservations.py +132 -0
  56. nwp500_python-9.2.1/tests/test_tou_schedule.py +192 -0
  57. nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  58. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.coveragerc +0 -0
  59. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  60. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/copilot-instructions.md +0 -0
  61. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/workflows/ci.yml +0 -0
  62. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/workflows/release.yml +0 -0
  63. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.gitignore +0 -0
  64. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.pre-commit-config.yaml +0 -0
  65. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.readthedocs.yml +0 -0
  66. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/AUTHORS.rst +0 -0
  67. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/CONTRIBUTING.rst +0 -0
  68. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/LICENSE.txt +0 -0
  69. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/Makefile +0 -0
  70. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/README.rst +0 -0
  71. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/RELEASE.md +0 -0
  72. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/Makefile +0 -0
  73. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/_static/.gitignore +0 -0
  74. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/conf.py +0 -0
  75. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/advanced-features.rst +0 -0
  76. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/architecture.rst +0 -0
  77. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/index.rst +0 -0
  78. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/authenticate.rst +0 -0
  79. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/auto-recovery.rst +0 -0
  80. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/diagnose-mqtt.rst +0 -0
  81. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/home-assistant.rst +0 -0
  82. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/index.rst +0 -0
  83. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/maintenance.rst +0 -0
  84. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/manage-units.rst +0 -0
  85. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/monitor-status.rst +0 -0
  86. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/optimize-tou.rst +0 -0
  87. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/queue-commands.rst +0 -0
  88. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/schedule-operation.rst +0 -0
  89. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/track-energy.rst +0 -0
  90. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/index.rst +0 -0
  91. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/openapi.yaml +0 -0
  92. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/authors.rst +0 -0
  93. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/changelog.rst +0 -0
  94. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/contributing.rst +0 -0
  95. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/history.rst +0 -0
  96. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/license.rst +0 -0
  97. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/configuration.rst +0 -0
  98. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/enumerations.rst +0 -0
  99. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/index.rst +0 -0
  100. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/installation.rst +0 -0
  101. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/data_conversions.rst +0 -0
  102. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_features.rst +0 -0
  103. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_status.rst +0 -0
  104. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/error_codes.rst +0 -0
  105. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  106. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/quick_reference.rst +0 -0
  107. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/rest_api.rst +0 -0
  108. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/api_client.rst +0 -0
  109. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/auth_client.rst +0 -0
  110. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/cli.rst +0 -0
  111. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/events.rst +0 -0
  112. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/exceptions.rst +0 -0
  113. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/models.rst +0 -0
  114. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/mqtt_client.rst +0 -0
  115. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/requirements.txt +0 -0
  116. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/tutorials/getting-started.rst +0 -0
  117. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/.ruff.toml +0 -0
  118. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/README.md +0 -0
  119. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/tou_openei.py +0 -0
  120. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/03_get_status.py +0 -0
  121. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/error_handling.py +0 -0
  122. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/improved_auth.py +0 -0
  123. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
  124. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/pyproject.toml +0 -0
  125. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/README.md +0 -0
  126. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/bump_version.py +0 -0
  127. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/diagnose_mqtt_connection.py +0 -0
  128. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/extract_changelog.py +0 -0
  129. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/format.py +0 -0
  130. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/lint.py +0 -0
  131. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/setup-dev.py +0 -0
  132. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/validate_version.py +0 -0
  133. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/setup.cfg +0 -0
  134. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/setup.py +0 -0
  135. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/_base.py +0 -0
  136. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/api_client.py +0 -0
  137. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/auth.py +0 -0
  138. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/__init__.py +0 -0
  139. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/__main__.py +0 -0
  140. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/handlers.py +0 -0
  141. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/monitoring.py +0 -0
  142. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/output_formatters.py +0 -0
  143. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/presentation.py +0 -0
  144. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/rich_output.py +0 -0
  145. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/token_storage.py +0 -0
  146. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/command_decorators.py +0 -0
  147. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/config.py +0 -0
  148. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/converters.py +0 -0
  149. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/device_info_cache.py +0 -0
  150. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/enums.py +0 -0
  151. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/events.py +0 -0
  152. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/exceptions.py +0 -0
  153. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/factory.py +0 -0
  154. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/field_factory.py +0 -0
  155. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/__init__.py +0 -0
  156. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/_converters.py +0 -0
  157. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/device.py +0 -0
  158. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/energy.py +0 -0
  159. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/feature.py +0 -0
  160. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/mqtt_models.py +0 -0
  161. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/status.py +0 -0
  162. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/__init__.py +0 -0
  163. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/_control_commands.py +0 -0
  164. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  165. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/client.py +0 -0
  166. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/command_queue.py +0 -0
  167. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/connection.py +0 -0
  168. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/diagnostics.py +0 -0
  169. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/periodic.py +0 -0
  170. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/reconnection.py +0 -0
  171. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/state_tracker.py +0 -0
  172. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/subscriptions.py +0 -0
  173. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/types.py +0 -0
  174. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/utils.py +0 -0
  175. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt_events.py +0 -0
  176. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/openei.py +0 -0
  177. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/py.typed +0 -0
  178. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/temperature.py +0 -0
  179. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/topic_builder.py +0 -0
  180. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/unit_system.py +0 -0
  181. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/utils.py +0 -0
  182. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  183. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  184. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  185. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/requires.txt +0 -0
  186. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
  187. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/conftest.py +0 -0
  188. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_api_helpers.py +0 -0
  189. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_auth.py +0 -0
  190. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_auth_session_lifecycle.py +0 -0
  191. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_bug_fixes.py +0 -0
  192. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_cli_basic.py +0 -0
  193. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_cli_commands.py +0 -0
  194. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_command_decorators.py +0 -0
  195. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_command_queue.py +0 -0
  196. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_device_info_cache.py +0 -0
  197. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_events.py +0 -0
  198. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_exceptions.py +0 -0
  199. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_model_converters.py +0 -0
  200. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_models.py +0 -0
  201. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_clean_session_resume.py +0 -0
  202. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_client_init.py +0 -0
  203. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_events.py +0 -0
  204. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_hypothesis.py +0 -0
  205. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection.py +0 -0
  206. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection_storm.py +0 -0
  207. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reliability.py +0 -0
  208. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_multi_device.py +0 -0
  209. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_openei.py +0 -0
  210. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_public_api.py +0 -0
  211. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_temperature_converters.py +0 -0
  212. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_threading_model.py +0 -0
  213. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_token_storage.py +0 -0
  214. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_tou_api.py +0 -0
  215. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_unit_switching.py +0 -0
  216. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_unit_system_process_wide.py +0 -0
  217. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_utility_modules.py +0 -0
  218. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_utils.py +0 -0
  219. {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tox.ini +0 -0
@@ -5,6 +5,65 @@ 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
+
8
67
  Version 9.2.0 (2026-07-06)
9
68
  ==========================
10
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.2.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
@@ -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()
@@ -17,20 +17,23 @@ logging.basicConfig(
17
17
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
18
  )
19
19
 
20
+ logger = logging.getLogger(__name__)
21
+
20
22
  # If running from examples directory, add parent to path
21
23
  if __name__ == "__main__":
22
24
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
23
25
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
24
26
 
27
+ import re
28
+
25
29
  from nwp500 import NavienAPIClient
26
30
  from nwp500.auth import NavienAuthClient
27
31
  from nwp500.exceptions import (
28
32
  APIError,
29
33
  AuthenticationError,
34
+ Nwp500Error,
30
35
  )
31
36
 
32
- import re
33
-
34
37
 
35
38
  def mask_mac(mac: str) -> str:
36
39
  """Redact all MAC addresses in the input string."""
@@ -72,7 +75,7 @@ async def example_basic_usage():
72
75
  # Display device information
73
76
  try:
74
77
  from mask import mask_mac # type: ignore
75
- except Exception:
78
+ except ImportError:
76
79
  # fallback helper if import fails when running examples directly
77
80
 
78
81
  def mask_mac(mac: str) -> str: # pragma: no cover - small fallback
@@ -81,7 +84,7 @@ async def example_basic_usage():
81
84
 
82
85
  try:
83
86
  from mask import mask_any, mask_location # type: ignore
84
- except Exception:
87
+ except ImportError:
85
88
 
86
89
  def mask_any(_):
87
90
  return "[REDACTED]"
@@ -172,7 +175,7 @@ async def example_basic_usage():
172
175
 
173
176
  except Exception:
174
177
  # Avoid printing raw exception details to stdout in examples
175
- logging.exception("Unexpected error in api_client_example")
178
+ logger.exception("Unexpected error in api_client_example")
176
179
  return 1
177
180
 
178
181
 
@@ -199,7 +202,7 @@ async def example_convenience_function():
199
202
 
200
203
  try:
201
204
  from mask import mask_any, mask_location # type: ignore
202
- except Exception:
205
+ except ImportError:
203
206
 
204
207
  def mask_any(_):
205
208
  return "[REDACTED]"
@@ -218,8 +221,8 @@ async def example_convenience_function():
218
221
 
219
222
  return 0
220
223
 
221
- except Exception as e:
222
- print(f"[ERROR] Error: {str(e)}")
224
+ except Nwp500Error as e:
225
+ print(f"[ERROR] Error: {e!s}")
223
226
  return 1
224
227
 
225
228
 
@@ -8,7 +8,8 @@ This demonstrates how to programmatically change the water heater DHW
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 temperature change process
14
15
  logging.basicConfig(level=logging.INFO)
@@ -16,6 +16,7 @@ from nwp500 import (
16
16
  NavienAuthClient,
17
17
  NavienMqttClient,
18
18
  )
19
+ from nwp500.exceptions import Nwp500Error
19
20
 
20
21
 
21
22
  async def example_basic_pattern():
@@ -109,7 +110,7 @@ async def example_with_mqtt():
109
110
  await mqtt_client.disconnect()
110
111
  print("✓ MQTT Disconnected")
111
112
 
112
- except Exception as e:
113
+ except Nwp500Error as e:
113
114
  print(f"✗ MQTT error: {e}")
114
115
 
115
116
  print("✓ Context exited, session closed")
@@ -26,6 +26,7 @@ logging.basicConfig(
26
26
  )
27
27
 
28
28
  from nwp500.auth import NavienAuthClient
29
+ from nwp500.exceptions import Nwp500Error
29
30
  from nwp500.mqtt import MqttConnectionConfig, NavienMqttClient
30
31
 
31
32
 
@@ -197,7 +198,7 @@ async def command_queue_demo():
197
198
 
198
199
  return True
199
200
 
200
- except Exception as e:
201
+ except Nwp500Error as e:
201
202
  print(f"\n[ERROR] Error: {e}")
202
203
  import traceback
203
204
 
@@ -29,6 +29,8 @@ logging.getLogger("nwp500.mqtt_client").setLevel(logging.DEBUG)
29
29
  logging.getLogger("nwp500.auth").setLevel(logging.INFO)
30
30
  logging.getLogger("nwp500.api_client").setLevel(logging.INFO)
31
31
 
32
+ logger = logging.getLogger(__name__)
33
+
32
34
  # If running from examples directory, add parent to path
33
35
  if __name__ == "__main__":
34
36
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
@@ -42,7 +44,7 @@ from nwp500.mqtt import NavienMqttClient
42
44
 
43
45
  try:
44
46
  from mask import mask_mac, mask_mac_in_topic # type: ignore
45
- except Exception:
47
+ except ImportError:
46
48
 
47
49
  def mask_mac(mac): # pragma: no cover - fallback
48
50
  return "[REDACTED_MAC]"
@@ -236,9 +238,7 @@ async def main():
236
238
  print("[SUCCESS] Disconnected successfully")
237
239
 
238
240
  except Exception:
239
- import logging
240
-
241
- logging.exception("MQTT error in device_status_callback")
241
+ logger.exception("MQTT error in device_status_callback")
242
242
 
243
243
  if mqtt_client.is_connected:
244
244
  await mqtt_client.disconnect()
@@ -258,9 +258,7 @@ async def main():
258
258
  return 1
259
259
 
260
260
  except Exception:
261
- import logging
262
-
263
- logging.exception("Unexpected error in device_status_callback")
261
+ logger.exception("Unexpected error in device_status_callback")
264
262
  return 1
265
263
 
266
264