nwp500-python 9.0.0__tar.gz → 9.2.0__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 (216) hide show
  1. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/CHANGELOG.rst +109 -0
  2. {nwp500_python-9.0.0/src/nwp500_python.egg-info → nwp500_python-9.2.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/events.rst +17 -0
  4. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/bump_version.py +86 -16
  5. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/__init__.py +2 -0
  6. nwp500_python-9.2.0/src/nwp500/cli/output_formatters.py +172 -0
  7. nwp500_python-9.0.0/src/nwp500/cli/output_formatters.py → nwp500_python-9.2.0/src/nwp500/cli/presentation.py +189 -358
  8. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/rich_output.py +144 -402
  9. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/events.py +20 -3
  10. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/feature.py +20 -2
  11. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/schedule.py +20 -0
  12. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/status.py +20 -2
  13. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/tou.py +8 -0
  14. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/__init__.py +3 -0
  15. nwp500_python-9.2.0/src/nwp500/mqtt/_control_commands.py +217 -0
  16. nwp500_python-9.2.0/src/nwp500/mqtt/_device_subscriptions.py +227 -0
  17. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/client.py +22 -401
  18. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/command_queue.py +2 -5
  19. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/connection.py +49 -9
  20. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/state_tracker.py +8 -7
  21. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/subscriptions.py +80 -11
  22. nwp500_python-9.2.0/src/nwp500/mqtt/types.py +50 -0
  23. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/utils.py +2 -3
  24. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt_events.py +14 -2
  25. {nwp500_python-9.0.0 → nwp500_python-9.2.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  26. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/SOURCES.txt +6 -0
  27. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/scm_file_list.json +6 -0
  28. nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  29. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_command_queue.py +3 -4
  30. nwp500_python-9.2.0/tests/test_mqtt_events.py +120 -0
  31. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reliability.py +86 -2
  32. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_protocol_correctness.py +7 -9
  33. nwp500_python-9.2.0/tests/test_unit_system_process_wide.py +133 -0
  34. nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  35. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.coveragerc +0 -0
  36. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  37. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/copilot-instructions.md +0 -0
  38. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/workflows/ci.yml +0 -0
  39. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/workflows/release.yml +0 -0
  40. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.gitignore +0 -0
  41. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.pre-commit-config.yaml +0 -0
  42. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.readthedocs.yml +0 -0
  43. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/AUTHORS.rst +0 -0
  44. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/CONTRIBUTING.rst +0 -0
  45. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/LICENSE.txt +0 -0
  46. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/Makefile +0 -0
  47. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/README.rst +0 -0
  48. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/RELEASE.md +0 -0
  49. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/Makefile +0 -0
  50. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/_static/.gitignore +0 -0
  51. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/conf.py +0 -0
  52. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/advanced-features.rst +0 -0
  53. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/architecture.rst +0 -0
  54. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/index.rst +0 -0
  55. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/authenticate.rst +0 -0
  56. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/auto-recovery.rst +0 -0
  57. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  58. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/home-assistant.rst +0 -0
  59. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/index.rst +0 -0
  60. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/maintenance.rst +0 -0
  61. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/manage-units.rst +0 -0
  62. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/monitor-status.rst +0 -0
  63. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/optimize-tou.rst +0 -0
  64. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/queue-commands.rst +0 -0
  65. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/schedule-operation.rst +0 -0
  66. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/track-energy.rst +0 -0
  67. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/index.rst +0 -0
  68. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/openapi.yaml +0 -0
  69. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/authors.rst +0 -0
  70. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/changelog.rst +0 -0
  71. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/contributing.rst +0 -0
  72. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/history.rst +0 -0
  73. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/license.rst +0 -0
  74. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/configuration.rst +0 -0
  75. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/enumerations.rst +0 -0
  76. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/index.rst +0 -0
  77. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/installation.rst +0 -0
  78. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/data_conversions.rst +0 -0
  79. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/device_features.rst +0 -0
  80. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/device_status.rst +0 -0
  81. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/error_codes.rst +0 -0
  82. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  83. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/quick_reference.rst +0 -0
  84. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/rest_api.rst +0 -0
  85. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/api_client.rst +0 -0
  86. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/auth_client.rst +0 -0
  87. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/cli.rst +0 -0
  88. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/exceptions.rst +0 -0
  89. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/models.rst +0 -0
  90. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  91. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/requirements.txt +0 -0
  92. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/tutorials/getting-started.rst +0 -0
  93. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/.ruff.toml +0 -0
  94. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/README.md +0 -0
  95. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/air_filter_reset.py +0 -0
  96. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/anti_legionella.py +0 -0
  97. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/auto_recovery.py +0 -0
  98. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/combined_callbacks.py +0 -0
  99. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/demand_response.py +0 -0
  100. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/device_capabilities.py +0 -0
  101. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/device_status_debug.py +0 -0
  102. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/energy_analytics.py +0 -0
  103. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/error_code_demo.py +0 -0
  104. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/firmware_payload_capture.py +0 -0
  105. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  106. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/power_control.py +0 -0
  107. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/recirculation_control.py +0 -0
  108. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/reconnection_demo.py +0 -0
  109. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/reservation_schedule.py +0 -0
  110. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/simple_auto_recovery.py +0 -0
  111. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/token_restoration.py +0 -0
  112. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/tou_openei.py +0 -0
  113. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/tou_schedule.py +0 -0
  114. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/water_reservation.py +0 -0
  115. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/01_authentication.py +0 -0
  116. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/02_list_devices.py +0 -0
  117. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/03_get_status.py +0 -0
  118. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/04_set_temperature.py +0 -0
  119. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  120. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/command_queue.py +0 -0
  121. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/device_status_callback.py +0 -0
  122. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/error_handling.py +0 -0
  123. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/event_driven_control.py +0 -0
  124. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/improved_auth.py +0 -0
  125. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  126. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  127. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/periodic_requests.py +0 -0
  128. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/set_mode.py +0 -0
  129. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/vacation_mode.py +0 -0
  130. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/mask.py +0 -0
  131. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/periodic_device_info.py +0 -0
  132. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/simple_periodic_info.py +0 -0
  133. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_api_client.py +0 -0
  134. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_mqtt_connection.py +0 -0
  135. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_mqtt_messaging.py +0 -0
  136. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_periodic_minimal.py +0 -0
  137. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/pyproject.toml +0 -0
  138. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/README.md +0 -0
  139. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/diagnose_mqtt_connection.py +0 -0
  140. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/extract_changelog.py +0 -0
  141. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/format.py +0 -0
  142. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/lint.py +0 -0
  143. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/setup-dev.py +0 -0
  144. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/validate_version.py +0 -0
  145. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/setup.cfg +0 -0
  146. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/setup.py +0 -0
  147. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/_base.py +0 -0
  148. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/api_client.py +0 -0
  149. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/auth.py +0 -0
  150. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/__init__.py +0 -0
  151. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/__main__.py +0 -0
  152. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/handlers.py +0 -0
  153. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/monitoring.py +0 -0
  154. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/token_storage.py +0 -0
  155. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/command_decorators.py +0 -0
  156. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/config.py +0 -0
  157. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/converters.py +0 -0
  158. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/device_capabilities.py +0 -0
  159. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/device_info_cache.py +0 -0
  160. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/encoding.py +0 -0
  161. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/enums.py +0 -0
  162. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/exceptions.py +0 -0
  163. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/factory.py +0 -0
  164. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/field_factory.py +0 -0
  165. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/__init__.py +0 -0
  166. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/_converters.py +0 -0
  167. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/device.py +0 -0
  168. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/energy.py +0 -0
  169. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/mqtt_models.py +0 -0
  170. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/control.py +0 -0
  171. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  172. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/periodic.py +0 -0
  173. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/reconnection.py +0 -0
  174. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/openei.py +0 -0
  175. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/py.typed +0 -0
  176. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/reservations.py +0 -0
  177. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/temperature.py +0 -0
  178. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/topic_builder.py +0 -0
  179. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/unit_system.py +0 -0
  180. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/utils.py +0 -0
  181. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  182. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  183. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  184. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  185. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  186. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/conftest.py +0 -0
  187. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_api_helpers.py +0 -0
  188. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_auth.py +0 -0
  189. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_auth_session_lifecycle.py +0 -0
  190. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_bug_fixes.py +0 -0
  191. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_cli_basic.py +0 -0
  192. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_cli_commands.py +0 -0
  193. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_command_decorators.py +0 -0
  194. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_device_capabilities.py +0 -0
  195. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_device_info_cache.py +0 -0
  196. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_events.py +0 -0
  197. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_exceptions.py +0 -0
  198. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_model_converters.py +0 -0
  199. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_models.py +0 -0
  200. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  201. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_client_init.py +0 -0
  202. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_hypothesis.py +0 -0
  203. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection.py +0 -0
  204. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  205. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_multi_device.py +0 -0
  206. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_openei.py +0 -0
  207. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_public_api.py +0 -0
  208. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_reservations.py +0 -0
  209. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_temperature_converters.py +0 -0
  210. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_threading_model.py +0 -0
  211. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_token_storage.py +0 -0
  212. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_tou_api.py +0 -0
  213. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_unit_switching.py +0 -0
  214. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_utility_modules.py +0 -0
  215. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_utils.py +0 -0
  216. {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tox.ini +0 -0
@@ -5,6 +5,115 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.2.0 (2026-07-06)
9
+ ==========================
10
+
11
+ Changed
12
+ -------
13
+ - **CLI formatting stacks merged; Rich is the sole human renderer**
14
+ (`#100 <https://github.com/eman/nwp500-python/issues/100>`_): a new
15
+ ``src/nwp500/cli/presentation.py`` owns all data-shaping for CLI output
16
+ (field selection, labels, units, ordering, value formatting and energy
17
+ aggregation) as presentation-neutral structures. Because the CLI
18
+ hard-requires ``rich`` (there is no plain-text fallback), the redundant
19
+ plain-text human renderer and the Rich-vs-plain fallback machinery were
20
+ removed: ``rich_output.py`` no longer contains ``_should_use_rich``,
21
+ ``_rich_available``, the ``NWP500_NO_RICH`` toggle, or any
22
+ ``_print_*_plain`` methods, and now renders the neutral structures
23
+ (including the energy ``TOTAL SUMMARY``) exclusively with Rich, consuming
24
+ the presentation dataclasses directly instead of dict adapters.
25
+ ``output_formatters.py`` keeps only JSON/CSV rendering plus thin
26
+ human-output dispatch. Net CLI code drops from ~2088 to ~1838 lines with a
27
+ single data-shaping layer and a single human renderer. Non-energy output
28
+ is byte-for-byte unchanged (verified by golden capture and the existing
29
+ CLI tests); energy output now renders a summary table plus a breakdown
30
+ table entirely in Rich rather than printing plain text and a Rich table.
31
+ - **mqtt/client.py slimmed toward a thin façade** (`#99
32
+ <https://github.com/eman/nwp500-python/issues/99>`_): the ~40 device
33
+ control command proxies and the typed ``subscribe_*``/``unsubscribe_*``
34
+ device-subscription proxies were moved out of ``NavienMqttClient`` into
35
+ two focused mixins, ``DeviceControlCommandsMixin``
36
+ (``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
37
+ (``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
38
+ both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
39
+ from ~1572 to ~1196 lines and reads more clearly as connection
40
+ orchestration plus a public façade. No behavior change.
41
+ - **Event system relationship clarified and de-duplicated** (`#102
42
+ <https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
43
+ ``mqtt_events.py`` are not two competing event mechanisms.
44
+ ``EventEmitter`` (``events.py``) is the sole delivery mechanism, while
45
+ ``mqtt_events.py`` provides the event-name registry
46
+ (``MqttClientEvents``) and the typed dataclass payloads it carries.
47
+ Internal ``emit`` call sites in ``mqtt/state_tracker.py``,
48
+ ``mqtt/client.py`` and ``mqtt/subscriptions.py`` now reference the
49
+ ``MqttClientEvents`` constants instead of duplicating the raw event-name
50
+ strings, so each event name is defined in exactly one place. Module
51
+ docstrings and the event-system reference docs were updated to document
52
+ the relationship, and typed-payload delivery is now covered by tests.
53
+ - **awscrt types wrapped out of public MQTT signatures** (`#101
54
+ <https://github.com/eman/nwp500-python/issues/101>`_): the library no
55
+ longer exposes ``awscrt`` SDK types on its own public/semi-public API
56
+ surface. A library-owned ``nwp500.mqtt.QoS`` ``IntEnum`` (also exported
57
+ as ``nwp500.QoS``) now replaces ``awscrt.mqtt.QoS`` on the ``publish``
58
+ and ``subscribe`` methods of ``NavienMqttClient``, ``MqttConnection``
59
+ and ``MqttSubscriptionManager``, on ``MqttCommandQueue.enqueue`` and on
60
+ the ``QueuedCommand`` dataclass. ``awscrt.mqtt.Connection`` handles are
61
+ typed behind the ``MqttConnectionHandle`` alias, and translation to/from
62
+ ``awscrt`` happens only at the connection-layer boundary
63
+ (``nwp500/mqtt/types.py``). ``NavienMqttClient.publish`` now wraps
64
+ otherwise-uncaught ``AwsCrtError`` in ``MqttPublishError`` so ``awscrt``
65
+ exceptions no longer leak out of the public boundary.
66
+
67
+ .. code-block:: python
68
+
69
+ # OLD
70
+ from awscrt import mqtt
71
+ await client.publish(topic, payload, qos=mqtt.QoS.AT_LEAST_ONCE)
72
+
73
+ # NEW
74
+ from nwp500 import QoS
75
+ await client.publish(topic, payload, qos=QoS.AT_LEAST_ONCE)
76
+
77
+ Fixed
78
+ -----
79
+ - **MQTT ack futures now consumed on abandonment** (`#97
80
+ <https://github.com/eman/nwp500-python/issues/97>`_): ``_await_ack()``
81
+ in ``mqtt/connection.py`` and the inline subscribe/unsubscribe
82
+ acknowledgement waits in ``mqtt/subscriptions.py`` shield the AWS CRT
83
+ future so a timeout or cancellation doesn't propagate into the SDK
84
+ future. Previously, if the shielded future later completed with an
85
+ exception (e.g. ``AwsCrtError`` from clean-session cancellation during
86
+ reconnect) after the awaiting task had already given up, nobody
87
+ retrieved that exception, and asyncio logged a "Future exception was
88
+ never retrieved" warning at garbage-collection time. A done callback
89
+ is now attached whenever a wait is abandoned so the eventual
90
+ result/exception is always retrieved and logged at debug level
91
+ instead of leaking as an unhandled asyncio warning.
92
+
93
+ Documentation
94
+ -------------
95
+ - **Unit-system preference is a deliberate process-wide global** (`#103
96
+ <https://github.com/eman/nwp500-python/issues/103>`_): clarified and
97
+ locked in that the preference in ``nwp500.unit_system`` is an intentional
98
+ process-wide module-level global rather than a
99
+ ``contextvars.ContextVar``. A context-local value silently reverted to
100
+ auto-detect for real-time data because MQTT message handling runs in tasks
101
+ scheduled from AWS CRT callback threads whose context never inherits from
102
+ the application task. Added prominent docstring notes to the affected
103
+ models (``DeviceStatus``, ``DeviceFeature``, ``ReservationEntry``,
104
+ ``WeeklyReservationEntry``, and ``TOUPeriod``) explaining that unit-aware
105
+ computed fields read the preference at access time and share it across all
106
+ async tasks and threads. This is a non-breaking documentation and test
107
+ change; no API changes.
108
+
109
+ Added
110
+ -----
111
+ - **Tests for process-wide unit-system semantics** (`#103
112
+ <https://github.com/eman/nwp500-python/issues/103>`_): new
113
+ ``tests/test_unit_system_process_wide.py`` verifies that setting the global
114
+ preference affects already-constructed model instances at access time and
115
+ that the preference is visible across async tasks and threads.
116
+
8
117
  Version 9.0.0 (2026-07-05)
9
118
  ==========================
10
119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.0.0
3
+ Version: 9.2.0
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
 
@@ -20,17 +20,20 @@ field is for the PyScaffold tool version, not the package version!
20
20
  """
21
21
 
22
22
  import re
23
+ import shutil
23
24
  import subprocess
24
25
  import sys
25
- from datetime import date
26
+ from datetime import datetime
26
27
  from pathlib import Path
27
28
 
29
+ GIT = shutil.which("git") or "git"
28
30
 
29
- def run_git_command(args: list) -> str:
31
+
32
+ def run_git_command(args: list[str]) -> str:
30
33
  """Run a git command and return the output."""
31
34
  try:
32
- result = subprocess.run(
33
- ["git"] + args,
35
+ result = subprocess.run( # noqa: S603
36
+ [GIT, *args],
34
37
  capture_output=True,
35
38
  text=True,
36
39
  check=True,
@@ -142,6 +145,73 @@ def check_working_directory_clean() -> None:
142
145
  sys.exit(1)
143
146
 
144
147
 
148
+ def check_up_to_date_with_remote() -> None:
149
+ """Ensure the local branch is in sync with its remote before bumping.
150
+
151
+ Running the version bump from a stale local checkout can silently drop
152
+ CHANGELOG.rst entries: the local file is missing changes (e.g. a prior
153
+ release's changelog section) that already exist upstream, so the diff
154
+ inserted by update_changelog() is based on outdated content and the
155
+ upstream entry never makes it back in once pushed/merged.
156
+ """
157
+ branch = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
158
+ upstream = subprocess.run( # noqa: S603
159
+ [GIT, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
160
+ capture_output=True,
161
+ text=True,
162
+ )
163
+ if upstream.returncode != 0:
164
+ print(
165
+ f"Warning: Branch '{branch}' has no upstream tracking branch; "
166
+ "skipping remote sync check.",
167
+ file=sys.stderr,
168
+ )
169
+ return
170
+
171
+ upstream_ref = upstream.stdout.strip()
172
+
173
+ # Fetch quietly so the comparison below reflects the latest remote state.
174
+ fetch = subprocess.run( # noqa: S603
175
+ [GIT, "fetch", "--quiet"],
176
+ capture_output=True,
177
+ text=True,
178
+ )
179
+ if fetch.returncode != 0:
180
+ print(
181
+ f"Warning: 'git fetch' failed, remote sync check may be stale: "
182
+ f"{fetch.stderr.strip()}",
183
+ file=sys.stderr,
184
+ )
185
+
186
+ local_sha = run_git_command(["rev-parse", "HEAD"])
187
+ upstream_sha = run_git_command(["rev-parse", upstream_ref])
188
+
189
+ if local_sha == upstream_sha:
190
+ return
191
+
192
+ behind = run_git_command(["rev-list", "--count", f"HEAD..{upstream_ref}"])
193
+ ahead = run_git_command(["rev-list", "--count", f"{upstream_ref}..HEAD"])
194
+
195
+ if int(behind) > 0:
196
+ print(
197
+ f"Error: Local branch '{branch}' is {behind} commit(s) behind "
198
+ f"'{upstream_ref}'.",
199
+ file=sys.stderr,
200
+ )
201
+ print(
202
+ "Pull the latest changes before bumping the version, otherwise "
203
+ "CHANGELOG.rst entries from other releases may be lost.",
204
+ file=sys.stderr,
205
+ )
206
+ sys.exit(1)
207
+
208
+ if int(ahead) > 0:
209
+ print(
210
+ f"Warning: Local branch '{branch}' is {ahead} commit(s) ahead of "
211
+ f"'{upstream_ref}' (unpushed commits)."
212
+ )
213
+
214
+
145
215
  def update_changelog(version: str) -> None:
146
216
  """Insert a version heading into CHANGELOG.rst below the Unreleased section.
147
217
 
@@ -169,7 +239,8 @@ def update_changelog(version: str) -> None:
169
239
 
170
240
  content = changelog_path.read_text(encoding="utf-8")
171
241
 
172
- heading = f"Version {version} ({date.today().isoformat()})"
242
+ today = datetime.now().astimezone().date().isoformat()
243
+ heading = f"Version {version} ({today})"
173
244
  underline = "=" * len(heading)
174
245
  version_block = f"{heading}\n{underline}\n"
175
246
 
@@ -177,7 +248,7 @@ def update_changelog(version: str) -> None:
177
248
  # one or more blank lines, then insert the version block after them.
178
249
  pattern = re.compile(
179
250
  r"(Unreleased\n=+\n)" # group 1: Unreleased heading
180
- r"(\n+)", # group 2: blank line(s) separator
251
+ r"(\n+)", # group 2: blank line(s) separator
181
252
  re.MULTILINE,
182
253
  )
183
254
 
@@ -192,10 +263,7 @@ def update_changelog(version: str) -> None:
192
263
 
193
264
  # Insert the version block after the blank lines that follow "Unreleased"
194
265
  new_content = (
195
- content[: match.end()]
196
- + version_block
197
- + "\n"
198
- + content[match.end() :]
266
+ content[: match.end()] + version_block + "\n" + content[match.end() :]
199
267
  )
200
268
 
201
269
  changelog_path.write_text(new_content, encoding="utf-8")
@@ -205,20 +273,18 @@ def update_changelog(version: str) -> None:
205
273
  def commit_changelog(version: str) -> None:
206
274
  """Stage and commit the CHANGELOG.rst update."""
207
275
  run_git_command(["add", "CHANGELOG.rst"])
208
- run_git_command(
209
- ["commit", "-m", f"Update changelog for v{version}"]
210
- )
276
+ run_git_command(["commit", "-m", f"Update changelog for v{version}"])
211
277
  print("[OK] Committed changelog update")
212
278
 
213
279
 
214
- def create_tag(version: str, message: str = None) -> None:
280
+ def create_tag(version: str, message: str | None = None) -> None:
215
281
  """Create a git tag for the version."""
216
282
  tag_name = f"v{version}"
217
283
 
218
284
  # Check if tag already exists
219
285
  try:
220
- subprocess.run(
221
- ["git", "rev-parse", tag_name],
286
+ subprocess.run( # noqa: S603
287
+ [GIT, "rev-parse", tag_name],
222
288
  capture_output=True,
223
289
  check=True,
224
290
  )
@@ -283,6 +349,10 @@ def main() -> None:
283
349
  # Check working directory is clean
284
350
  check_working_directory_clean()
285
351
 
352
+ # Ensure we're not bumping from a stale checkout, which can silently
353
+ # drop CHANGELOG.rst entries from releases made on the remote branch.
354
+ check_up_to_date_with_remote()
355
+
286
356
  # Get current version
287
357
  current_version = get_current_version()
288
358
  print(f"Current version: {current_version}")
@@ -110,6 +110,7 @@ from nwp500.mqtt import (
110
110
  MqttMetrics,
111
111
  NavienMqttClient,
112
112
  PeriodicRequestType,
113
+ QoS,
113
114
  )
114
115
  from nwp500.mqtt_events import (
115
116
  MqttClientEvents,
@@ -221,6 +222,7 @@ __all__ = [
221
222
  "MqttMetrics",
222
223
  "ConnectionDropEvent",
223
224
  "ConnectionEvent",
225
+ "QoS",
224
226
  # Event Emitter
225
227
  "EventEmitter",
226
228
  "EventListener",
@@ -0,0 +1,172 @@
1
+ """CSV and JSON renderers plus human-output dispatch for the CLI.
2
+
3
+ Data-shaping (which fields, labels, units, ordering, aggregation) lives in
4
+ :mod:`.presentation`; human-readable rendering is delegated to the Rich
5
+ renderer in :mod:`.rich_output`, which consumes the same neutral structures.
6
+ This module additionally handles the genuinely different CSV and JSON outputs.
7
+ """
8
+
9
+ import csv
10
+ import json
11
+ import logging
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from nwp500 import DeviceStatus
18
+
19
+ from .presentation import (
20
+ build_daily_energy_report,
21
+ build_device_info_rows,
22
+ build_device_status_rows,
23
+ build_energy_report,
24
+ )
25
+ from .rich_output import get_formatter
26
+
27
+ _logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _json_default_serializer(obj: Any) -> Any:
31
+ """Serialize objects not serializable by default json code.
32
+
33
+ Note: Enums are handled by model.model_dump() which converts them to names.
34
+ This function handles any remaining non-JSON-serializable types that might
35
+ appear in raw MQTT messages.
36
+
37
+ Args:
38
+ obj: Object to serialize
39
+
40
+ Returns:
41
+ JSON-serializable representation of the object
42
+
43
+ Raises:
44
+ TypeError: If object cannot be serialized
45
+ """
46
+ if isinstance(obj, datetime):
47
+ return obj.isoformat()
48
+ if isinstance(obj, Enum):
49
+ return obj.name # Fallback for any enums not in model output
50
+ # Handle Pydantic models
51
+ if hasattr(obj, "model_dump"):
52
+ return obj.model_dump()
53
+ raise TypeError(f"Type {type(obj)} not serializable")
54
+
55
+
56
+ def print_energy_usage(energy_response: Any) -> None:
57
+ """Print energy usage data (summary + monthly breakdown) via Rich.
58
+
59
+ Args:
60
+ energy_response: EnergyUsageResponse object
61
+ """
62
+ report = build_energy_report(energy_response)
63
+ get_formatter().print_energy_table(report)
64
+
65
+
66
+ def print_daily_energy_usage(
67
+ energy_response: Any, year: int, month: int
68
+ ) -> None:
69
+ """Print daily energy usage for a specific month via Rich.
70
+
71
+ Args:
72
+ energy_response: EnergyUsageResponse object
73
+ year: Year to filter for (e.g., 2025)
74
+ month: Month to filter for (1-12)
75
+ """
76
+ report = build_daily_energy_report(energy_response, year, month)
77
+ formatter = get_formatter()
78
+ if report is None:
79
+ formatter.print_info(
80
+ f"No daily energy data available for {month}/{year}"
81
+ )
82
+ return
83
+ formatter.print_daily_energy_table(report)
84
+
85
+
86
+ def write_status_to_csv(file_path: str, status: DeviceStatus) -> None:
87
+ """
88
+ Append device status to a CSV file.
89
+
90
+ Args:
91
+ file_path: Path to the CSV file
92
+ status: DeviceStatus object to write
93
+ """
94
+ try:
95
+ # Convert status to dict (enums are already converted to names)
96
+ status_dict = status.model_dump()
97
+
98
+ # Add a timestamp to the beginning of the data (timezone-aware,
99
+ # in the local timezone)
100
+ status_dict["timestamp"] = datetime.now().astimezone().isoformat()
101
+
102
+ # Check if file exists to determine if we need to write the header
103
+ file_exists = Path(file_path).exists()
104
+
105
+ with Path(file_path).open("a", newline="") as csvfile:
106
+ # Get the field names from the dict keys
107
+ fieldnames = list(status_dict.keys())
108
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
109
+
110
+ # Write header only if this is a new file
111
+ if not file_exists:
112
+ writer.writeheader()
113
+
114
+ writer.writerow(status_dict)
115
+
116
+ _logger.debug(f"Status written to {file_path}")
117
+
118
+ except OSError as e:
119
+ _logger.error(f"Failed to write to CSV: {e}")
120
+
121
+
122
+ def format_json_output(data: Any, indent: int = 2) -> str:
123
+ """
124
+ Format data as JSON string with custom serialization.
125
+
126
+ Args:
127
+ data: Data to format
128
+ indent: Number of spaces for indentation (default: 2)
129
+
130
+ Returns:
131
+ JSON-formatted string
132
+ """
133
+ return json.dumps(data, indent=indent, default=_json_default_serializer)
134
+
135
+
136
+ def print_json(data: Any, indent: int = 2) -> None:
137
+ """
138
+ Print data as formatted JSON with optional syntax highlighting.
139
+
140
+ Uses Rich highlighting when available, falls back to plain JSON otherwise.
141
+
142
+ Args:
143
+ data: Data to print
144
+ indent: Number of spaces for indentation (default: 2)
145
+ """
146
+ json_str = format_json_output(data, indent)
147
+ formatter = get_formatter()
148
+ formatter.print_json_highlighted(json.loads(json_str))
149
+
150
+
151
+ def print_device_status(device_status: Any) -> None:
152
+ """
153
+ Print device status with aligned columns and dynamic width calculation.
154
+
155
+ Units are automatically extracted from the DeviceStatus model metadata.
156
+
157
+ Args:
158
+ device_status: DeviceStatus object
159
+ """
160
+ formatter = get_formatter()
161
+ formatter.print_status_table(build_device_status_rows(device_status))
162
+
163
+
164
+ def print_device_info(device_feature: Any) -> None:
165
+ """
166
+ Print device information with aligned columns and dynamic width calculation.
167
+
168
+ Args:
169
+ device_feature: DeviceFeature object
170
+ """
171
+ formatter = get_formatter()
172
+ formatter.print_status_table(build_device_info_rows(device_feature))