nwp500-python 8.1.3__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 (218) hide show
  1. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.gitignore +3 -0
  2. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/CHANGELOG.rst +411 -0
  3. {nwp500_python-8.1.3/src/nwp500_python.egg-info → nwp500_python-9.2.0}/PKG-INFO +1 -1
  4. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/optimize-tou.rst +7 -6
  5. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/schedule-operation.rst +3 -3
  6. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/auth_client.rst +0 -4
  7. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/cli.rst +4 -9
  8. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/events.rst +17 -0
  9. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/exceptions.rst +0 -57
  10. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/mqtt_client.rst +1 -1
  11. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/mqtt_diagnostics.py +4 -5
  12. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/tou_openei.py +2 -0
  13. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/mask.py +0 -2
  14. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/pyproject.toml +19 -0
  15. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/bump_version.py +86 -16
  16. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/__init__.py +3 -49
  17. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/_base.py +11 -2
  18. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/api_client.py +19 -7
  19. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/auth.py +124 -36
  20. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/__init__.py +0 -2
  21. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/__main__.py +7 -5
  22. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/handlers.py +2 -4
  23. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/monitoring.py +0 -2
  24. nwp500_python-9.2.0/src/nwp500/cli/output_formatters.py +172 -0
  25. nwp500_python-8.1.3/src/nwp500/cli/output_formatters.py → nwp500_python-9.2.0/src/nwp500/cli/presentation.py +186 -370
  26. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/rich_output.py +145 -404
  27. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/cli/token_storage.py +7 -4
  28. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/command_decorators.py +1 -3
  29. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/config.py +0 -2
  30. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/converters.py +0 -37
  31. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/device_capabilities.py +2 -4
  32. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/device_info_cache.py +0 -2
  33. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/encoding.py +39 -13
  34. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/enums.py +0 -2
  35. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/events.py +42 -25
  36. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/exceptions.py +0 -59
  37. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/factory.py +0 -2
  38. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/field_factory.py +57 -67
  39. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/__init__.py +0 -2
  40. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/_converters.py +0 -2
  41. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/device.py +0 -2
  42. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/energy.py +0 -2
  43. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/feature.py +20 -4
  44. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/mqtt_models.py +0 -2
  45. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/schedule.py +20 -2
  46. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/status.py +26 -6
  47. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/models/tou.py +8 -2
  48. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/__init__.py +3 -2
  49. nwp500_python-9.2.0/src/nwp500/mqtt/_control_commands.py +217 -0
  50. nwp500_python-9.2.0/src/nwp500/mqtt/_device_subscriptions.py +227 -0
  51. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/client.py +97 -445
  52. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/command_queue.py +49 -63
  53. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/connection.py +117 -82
  54. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/control.py +16 -6
  55. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/diagnostics.py +0 -2
  56. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/periodic.py +20 -9
  57. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/reconnection.py +72 -15
  58. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/state_tracker.py +12 -11
  59. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/subscriptions.py +141 -48
  60. nwp500_python-9.2.0/src/nwp500/mqtt/types.py +50 -0
  61. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt/utils.py +37 -7
  62. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/mqtt_events.py +26 -16
  63. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/openei.py +15 -3
  64. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/reservations.py +0 -2
  65. nwp500_python-9.2.0/src/nwp500/temperature.py +254 -0
  66. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/topic_builder.py +0 -2
  67. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/unit_system.py +24 -23
  68. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/utils.py +0 -2
  69. {nwp500_python-8.1.3 → nwp500_python-9.2.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  70. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/SOURCES.txt +15 -1
  71. nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_file_list.json +209 -0
  72. nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  73. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_auth.py +2 -11
  74. nwp500_python-9.2.0/tests/test_auth_session_lifecycle.py +275 -0
  75. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_bug_fixes.py +2 -3
  76. nwp500_python-9.2.0/tests/test_cli_basic.py +111 -0
  77. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_cli_commands.py +15 -0
  78. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_command_queue.py +3 -4
  79. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_events.py +2 -1
  80. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_exceptions.py +0 -39
  81. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_mqtt_clean_session_resume.py +0 -2
  82. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_mqtt_client_init.py +49 -0
  83. nwp500_python-9.2.0/tests/test_mqtt_events.py +120 -0
  84. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection.py +0 -2
  85. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection_storm.py +1 -3
  86. nwp500_python-9.2.0/tests/test_mqtt_reliability.py +714 -0
  87. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_multi_device.py +2 -2
  88. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_openei.py +23 -0
  89. nwp500_python-9.2.0/tests/test_protocol_correctness.py +453 -0
  90. nwp500_python-9.2.0/tests/test_public_api.py +61 -0
  91. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_reservations.py +0 -2
  92. nwp500_python-9.2.0/tests/test_threading_model.py +305 -0
  93. nwp500_python-9.2.0/tests/test_token_storage.py +61 -0
  94. nwp500_python-9.2.0/tests/test_unit_system_process_wide.py +133 -0
  95. nwp500_python-9.2.0/tests/test_utility_modules.py +149 -0
  96. nwp500_python-8.1.3/src/nwp500/cli/commands.py +0 -91
  97. nwp500_python-8.1.3/src/nwp500/temperature.py +0 -447
  98. nwp500_python-8.1.3/tests/test_cli_basic.py +0 -27
  99. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.coveragerc +0 -0
  100. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  101. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.github/copilot-instructions.md +0 -0
  102. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.github/workflows/ci.yml +0 -0
  103. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.github/workflows/release.yml +0 -0
  104. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.pre-commit-config.yaml +0 -0
  105. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/.readthedocs.yml +0 -0
  106. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/AUTHORS.rst +0 -0
  107. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/CONTRIBUTING.rst +0 -0
  108. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/LICENSE.txt +0 -0
  109. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/Makefile +0 -0
  110. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/README.rst +0 -0
  111. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/RELEASE.md +0 -0
  112. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/Makefile +0 -0
  113. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/_static/.gitignore +0 -0
  114. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/conf.py +0 -0
  115. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/explanation/advanced-features.rst +0 -0
  116. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/explanation/architecture.rst +0 -0
  117. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/explanation/index.rst +0 -0
  118. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/authenticate.rst +0 -0
  119. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/auto-recovery.rst +0 -0
  120. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  121. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/home-assistant.rst +0 -0
  122. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/index.rst +0 -0
  123. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/maintenance.rst +0 -0
  124. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/manage-units.rst +0 -0
  125. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/monitor-status.rst +0 -0
  126. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/queue-commands.rst +0 -0
  127. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/how-to/track-energy.rst +0 -0
  128. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/index.rst +0 -0
  129. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/openapi.yaml +0 -0
  130. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/project/authors.rst +0 -0
  131. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/project/changelog.rst +0 -0
  132. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/project/contributing.rst +0 -0
  133. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/project/history.rst +0 -0
  134. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/project/license.rst +0 -0
  135. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/configuration.rst +0 -0
  136. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/enumerations.rst +0 -0
  137. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/index.rst +0 -0
  138. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/installation.rst +0 -0
  139. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/data_conversions.rst +0 -0
  140. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/device_features.rst +0 -0
  141. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/device_status.rst +0 -0
  142. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/error_codes.rst +0 -0
  143. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  144. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/quick_reference.rst +0 -0
  145. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/protocol/rest_api.rst +0 -0
  146. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/api_client.rst +0 -0
  147. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/reference/python_api/models.rst +0 -0
  148. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/requirements.txt +0 -0
  149. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/docs/tutorials/getting-started.rst +0 -0
  150. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/.ruff.toml +0 -0
  151. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/README.md +0 -0
  152. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/air_filter_reset.py +0 -0
  153. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/anti_legionella.py +0 -0
  154. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/auto_recovery.py +0 -0
  155. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/combined_callbacks.py +0 -0
  156. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/demand_response.py +0 -0
  157. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/device_capabilities.py +0 -0
  158. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/device_status_debug.py +0 -0
  159. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/energy_analytics.py +0 -0
  160. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/error_code_demo.py +0 -0
  161. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/firmware_payload_capture.py +0 -0
  162. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/power_control.py +0 -0
  163. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/recirculation_control.py +0 -0
  164. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/reconnection_demo.py +0 -0
  165. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/reservation_schedule.py +0 -0
  166. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/simple_auto_recovery.py +0 -0
  167. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/token_restoration.py +0 -0
  168. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/tou_schedule.py +0 -0
  169. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/advanced/water_reservation.py +0 -0
  170. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/beginner/01_authentication.py +0 -0
  171. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/beginner/02_list_devices.py +0 -0
  172. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/beginner/03_get_status.py +0 -0
  173. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/beginner/04_set_temperature.py +0 -0
  174. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  175. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/command_queue.py +0 -0
  176. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/device_status_callback.py +0 -0
  177. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/error_handling.py +0 -0
  178. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/event_driven_control.py +0 -0
  179. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/improved_auth.py +0 -0
  180. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  181. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  182. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/periodic_requests.py +0 -0
  183. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/set_mode.py +0 -0
  184. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/intermediate/vacation_mode.py +0 -0
  185. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/periodic_device_info.py +0 -0
  186. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/simple_periodic_info.py +0 -0
  187. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/test_api_client.py +0 -0
  188. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/test_mqtt_connection.py +0 -0
  189. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/test_mqtt_messaging.py +0 -0
  190. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/examples/testing/test_periodic_minimal.py +0 -0
  191. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/README.md +0 -0
  192. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/diagnose_mqtt_connection.py +0 -0
  193. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/extract_changelog.py +0 -0
  194. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/format.py +0 -0
  195. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/lint.py +0 -0
  196. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/setup-dev.py +0 -0
  197. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/scripts/validate_version.py +0 -0
  198. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/setup.cfg +0 -0
  199. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/setup.py +0 -0
  200. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500/py.typed +0 -0
  201. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  202. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  203. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  204. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  205. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  206. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/conftest.py +0 -0
  207. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_api_helpers.py +0 -0
  208. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_command_decorators.py +0 -0
  209. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_device_capabilities.py +0 -0
  210. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_device_info_cache.py +0 -0
  211. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_model_converters.py +0 -0
  212. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_models.py +0 -0
  213. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_mqtt_hypothesis.py +0 -0
  214. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_temperature_converters.py +0 -0
  215. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_tou_api.py +0 -0
  216. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_unit_switching.py +0 -0
  217. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tests/test_utils.py +0 -0
  218. {nwp500_python-8.1.3 → nwp500_python-9.2.0}/tox.ini +0 -0
@@ -68,3 +68,6 @@ resources/*
68
68
  .ruff_cache/
69
69
  .bandit
70
70
  .agent/
71
+ .agents/
72
+ .env
73
+ skills-lock.json
@@ -5,6 +5,417 @@ 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
+
117
+ Version 9.0.0 (2026-07-05)
118
+ ==========================
119
+
120
+ **BREAKING CHANGES**: Public API surface trimmed and dead code removed.
121
+ These changes require a major version bump.
122
+
123
+ Modernization (Python 3.14)
124
+ ---------------------------
125
+ - **Removed** ``from __future__ import annotations`` **project-wide**
126
+ (57 files): redundant on a Python >=3.14-only package where lazy
127
+ annotation evaluation (PEP 649) is the default.
128
+ - **Adopted additional ruff rule sets**: ``RUF`` (Ruff-specific),
129
+ ``DTZ`` (naive datetime), ``PTH`` (pathlib), ``ASYNC``, and ``PERF``,
130
+ with documented ignores for intentional patterns (grouped ``__all__``
131
+ lists, unicode degree signs, public ``timeout`` parameters). Fixes
132
+ applied for all resulting findings, including a timezone-naive
133
+ timestamp in CSV exports (now timezone-aware local time),
134
+ ``Path.open()`` usage, ``itertools.pairwise()`` in the CLI range
135
+ collapser, a ``ClassVar`` annotation on the capability map, and
136
+ dangling ``asyncio.create_task`` references in tests.
137
+ - **PeriodicRequestType is now a StrEnum** (was a plain ``Enum`` with
138
+ string values), matching the enum style used elsewhere.
139
+ - **Event payload dataclasses use** ``slots=True``: the frozen event
140
+ dataclasses in ``mqtt_events.py`` and ``events.EventListener`` are
141
+ created per state change; slots reduce their memory footprint.
142
+ - **match statement** for the periodic request-type dispatch.
143
+
144
+ Testing
145
+ -------
146
+ - New unit tests for previously untested modules:
147
+ ``topic_builder.py`` (full topic schema), ``field_factory.py``
148
+ (metadata defaults, overrides, merge semantics), and
149
+ ``models/_converters.py`` (unit-preference conversions and
150
+ round-trips).
151
+
152
+ Removed
153
+ -------
154
+ - **Deprecated ``.control`` property**: removed
155
+ ``NavienMqttClient.control`` (deprecated shim slated for v9.0.0; the
156
+ project policy is to remove rather than deprecate). Use the delegated
157
+ methods on the client directly:
158
+
159
+ .. code-block:: python
160
+
161
+ # OLD (removed)
162
+ await client.control.set_power(device, True)
163
+
164
+ # NEW
165
+ await client.set_power(device, True)
166
+
167
+ - **Unused exception classes**: removed ``TokenExpiredError``,
168
+ ``MqttSubscriptionError``, ``DeviceNotFoundError``,
169
+ ``DeviceOfflineError``, and ``DeviceOperationError``. None of them was
170
+ ever raised by the library, so no working error handling can break;
171
+ catch the parent classes (``AuthenticationError``, ``MqttError``,
172
+ ``DeviceError``) instead.
173
+ - **Internal plumbing removed from the top-level namespace**: the
174
+ package no longer re-exports ``requires_capability``,
175
+ ``MqttDeviceInfoCache``, ``MqttDeviceCapabilityChecker``,
176
+ ``log_performance``, or the bit-encoding helpers
177
+ (``encode_week_bitfield``, ``decode_week_bitfield``,
178
+ ``encode_season_bitfield``, ``decode_season_bitfield``,
179
+ ``encode_price``, ``decode_price``, ``build_reservation_entry``,
180
+ ``build_tou_period``). Import them from their owning modules:
181
+
182
+ .. code-block:: python
183
+
184
+ # OLD (removed)
185
+ from nwp500 import build_reservation_entry, encode_price
186
+
187
+ # NEW
188
+ from nwp500.encoding import build_reservation_entry, encode_price
189
+
190
+ - **Dead code**: removed the never-imported ``nwp500.cli.commands``
191
+ module (its metadata had drifted from the real click commands), the
192
+ unused ``converters.str_enum_validator``, the unused module-level
193
+ ``temperature.half_celsius_to_fahrenheit`` /
194
+ ``deci_celsius_to_fahrenheit`` wrappers, the no-op
195
+ ``NavienMqttClient._on_message_received`` placeholder, and unused
196
+ width calculations in the CLI formatters.
197
+
198
+ Changed
199
+ -------
200
+ - **Temperature classes deduplicated**: ``HalfCelsius``, ``DeciCelsius``,
201
+ ``RawCelsius``, and ``DeciCelsiusDelta`` were four near-identical
202
+ copies differing only in a scale constant. All conversions are now
203
+ implemented once on the ``Temperature`` base class with a per-class
204
+ ``_scale``; only special rounding (``RawCelsius``) and delta semantics
205
+ (``DeciCelsiusDelta``) are overridden. Behavior is unchanged
206
+ (~200 lines removed).
207
+ - **field_factory deduplicated**: the four field factories shared a
208
+ copy-pasted metadata-merge block, now extracted into a single private
209
+ helper. Behavior is unchanged.
210
+ - **Device boolean encoding centralized**: ``mqtt/control.py`` now uses
211
+ ``converters.device_bool_from_python()`` instead of inline
212
+ ``2 if enabled else 1`` literals.
213
+ - **Version resolution decoupled**: ``auth.py`` resolves the package
214
+ version from distribution metadata instead of ``from . import
215
+ __version__``, removing an order-dependent near-circular import.
216
+
217
+ Bug Fixes
218
+ ---------
219
+ - **Fix malformed weekly reservation and recirculation schedule
220
+ payloads**: ``update_weekly_reservation()`` and
221
+ ``configure_recirculation_schedule()`` dumped the schedule models
222
+ as-is, double-nesting the request (``request.reservation.reservation``
223
+ / ``request.schedule.schedule``) and leaking pydantic computed display
224
+ fields — including a unit-converted ``temperature`` alongside the raw
225
+ half-Celsius ``param`` — into device commands. Both now send the flat,
226
+ raw protocol shape used by ``update_reservations()``. A new
227
+ ``NavienBaseModel.to_protocol_dict()`` dumps only declared protocol
228
+ fields.
229
+ - **Preserve command order when a queued flush fails**: a command that
230
+ failed mid-flush was re-queued at the tail, behind commands queued
231
+ after it, inverting order-sensitive sequences (e.g. ``set_temp``
232
+ replayed before ``power_on``). The queue is now a deque and failed
233
+ commands are re-inserted at the front.
234
+ - **Expire stale queued commands**: queued commands stored a timestamp
235
+ that was never checked, so a multi-hour outage replayed hours-old
236
+ control commands (e.g. ``set_power``) to the appliance on reconnect.
237
+ Commands older than ``MqttConnectionConfig.max_queued_command_age``
238
+ (default 300 s, ``None`` to disable) are now discarded at send time.
239
+ - **Fix Fahrenheit conversion for sub-zero temperatures** (ASYMMETRIC
240
+ formula): the rounding used Python's floored ``%``, which is always
241
+ non-negative, while the firmware/app uses a truncated remainder. Raw
242
+ ``-11`` (-5.5 °C) decoded to 22 °F instead of the app's 23 °F. Now
243
+ uses ``math.fmod`` semantics.
244
+ - **Fix freeze protection default limits**: the defaults (43/65) were
245
+ Fahrenheit display values stored in raw half-Celsius fields, decoding
246
+ to 70.7 °F / 90.5 °F when the device omitted them. Corrected to raw
247
+ 12/20 (43 °F / 50 °F), matching the documented fixed limits.
248
+ - **Emit error_detected when the error code changes**: a transition
249
+ between two non-zero error codes (e.g. E799 → E407) emitted no event,
250
+ so consumers kept displaying the stale error.
251
+ - **Fix TOU price encoding**: ``encode_price()`` used banker's rounding,
252
+ under-encoding exact half values at even boundaries (0.125 at
253
+ ``decimal_point=2`` encoded to 12 instead of 13) — now uses
254
+ ``Decimal`` half-up rounding. ``build_tou_period()`` also treated
255
+ ``bool`` prices as pre-encoded integers (``True`` sent as price 1).
256
+ - **Fix protocol documentation errors**: ``decode_reservation_hex()``
257
+ documented the enable flag inverted (1=enabled instead of 2=enabled);
258
+ the ``build_reservation_entry()`` example showed ``week: 158`` for
259
+ Mon/Wed/Fri instead of the correct 84; temperature doctest examples
260
+ showed ``int`` raw values where ``float`` is returned.
261
+ - **Run MQTT message dispatch on the event loop**: JSON parsing, pydantic
262
+ model validation, and user callbacks all executed directly on the AWS
263
+ CRT network thread. A slow or blocking callback (e.g. the CLI monitor's
264
+ CSV writes) stalled all MQTT message processing, user callbacks ran on
265
+ an undocumented SDK thread where asyncio operations are unsafe, and the
266
+ handler registry could be mutated on the event loop while the CRT
267
+ thread iterated it (``RuntimeError: dictionary changed size during
268
+ iteration``, dropping the message — most likely during the
269
+ reconnect/resubscribe window). The awscrt callback now only marshals
270
+ the raw payload onto the event loop; parsing and dispatch run there,
271
+ iterating snapshots of the handler registries.
272
+ - **Stop one raising handler from aborting message delivery**: handler
273
+ dispatch caught only ``(TypeError, AttributeError, KeyError)``; a user
274
+ callback raising anything else (e.g. ``ValueError``) escaped into the
275
+ awscrt callback machinery and skipped the remaining handlers for that
276
+ message. Individual handler failures are now logged and isolated.
277
+ - **Make the unit system preference process-wide**:
278
+ ``set_unit_system()`` stored the preference in a ``ContextVar`` set in
279
+ the caller's task. Tasks scheduled from AWS CRT callback threads never
280
+ inherit that context, so the preference silently reverted to
281
+ auto-detect for all MQTT-delivered data — ``nwp-cli --unit-system
282
+ metric monitor`` logged temperatures in the device's native unit while
283
+ labeling them °C. The preference is now a process-wide setting visible
284
+ from every task and thread.
285
+ - **Fix once-listeners firing more than once**: ``EventEmitter.emit()``
286
+ removed one-time listeners only after invoking them, so a callback
287
+ that raised stayed registered forever, and two overlapping emits could
288
+ both fire the same once-listener. Once-listeners are now removed
289
+ before invocation.
290
+ - **Fix wait_for() leaking its listener on cancellation**:
291
+ ``EventEmitter.wait_for()`` removed its listener only on timeout; a
292
+ cancelled waiter left the listener registered until the event next
293
+ fired, setting a result on a dead future. Cleanup now happens in a
294
+ ``finally`` block.
295
+ - **Fix wait_for() docstring examples**: examples showed
296
+ ``args, _ = await emitter.wait_for(...)``, but ``wait_for`` returns
297
+ just the args tuple — following the documented pattern raised
298
+ ``ValueError`` or silently mis-assigned.
299
+ - **Serialize concurrent token refresh**: ``ensure_valid_token()`` and
300
+ ``refresh_token()`` had no lock, so concurrent callers (API 401 retry,
301
+ MQTT reconnect, periodic requests) at token expiry fired parallel
302
+ refresh requests; with token rotation the losers were left holding
303
+ invalidated tokens. Refreshes are now serialized behind an
304
+ ``asyncio.Lock`` with a post-acquire re-check: callers that lose the
305
+ race receive the already-refreshed tokens, callers passing a stale
306
+ (pre-rotation) refresh token get the fresh tokens instead of a
307
+ guaranteed failure, and an explicit refresh with the current token
308
+ still forces a refresh (deep-reconnect behavior preserved).
309
+ - **Preserve refresh_token/id_token across refreshes**: the refresh
310
+ response merge preserved AWS credential fields but not
311
+ ``refresh_token``/``id_token``. A refresh response omitting them wiped
312
+ the stored refresh token, so the next refresh posted an empty string
313
+ and failed unconditionally.
314
+ - **Fix aiohttp session leak when authentication fails in**
315
+ ``__aenter__``: Python never calls ``__aexit__`` when ``__aenter__``
316
+ raises, so bad credentials or a network error leaked the owned
317
+ ``ClientSession`` (one per retry attempt). The session is now closed
318
+ before the exception propagates.
319
+ - **Make** ``NavienAuthClient.__aenter__`` **idempotent**:
320
+ ``create_navien_clients()`` pre-enters the context and its docstring
321
+ instructs users to enter again with ``async with auth:``; the second
322
+ entry created a fresh session and orphaned the first — which the API
323
+ client was still pinned to. Re-entering now reuses the existing
324
+ session.
325
+ - **Fall back to full sign-in when stored-token refresh fails**:
326
+ restoring expired stored tokens raised ``TokenRefreshError`` even
327
+ though credentials for a full ``sign_in()`` were available.
328
+ - **Stop pinning the auth session in the API client**:
329
+ ``NavienAPIClient`` captured ``auth_client.session`` at construction
330
+ and kept using it after the auth client recreated its session
331
+ (``RuntimeError: Session is closed``). The session is now resolved per
332
+ request; an explicitly provided session still takes precedence.
333
+ - **Add HTTP timeouts to unguarded sessions**: the standalone
334
+ ``refresh_access_token()`` helper and ``OpenEIClient`` created
335
+ ``ClientSession``s without a ``ClientTimeout``; requests could hang
336
+ indefinitely. Both now use a 30-second total timeout.
337
+ - **Fix reconnection loop dying on authentication errors**: the backoff
338
+ loop caught only ``AwsCrtError`` and ``RuntimeError``, so
339
+ ``TokenRefreshError``, ``AuthenticationError``, and
340
+ ``MqttCredentialsError`` raised during quick/deep reconnection escaped
341
+ and silently killed the reconnect task. A routine outage coinciding
342
+ with token expiry left the client permanently offline despite unlimited
343
+ retries. All library errors (``Nwp500Error``) and operation timeouts
344
+ are now treated as failed attempts and retried; only
345
+ ``InvalidCredentialsError`` is fatal and stops the loop with a
346
+ ``reconnection_failed`` event.
347
+ - **Fix disconnect() being a no-op while the connection is interrupted**:
348
+ calling ``disconnect()`` during an interruption returned early without
349
+ disabling automatic reconnection or stopping periodic tasks, so the
350
+ backoff loop would resurrect the connection after the application shut
351
+ the client down. ``disconnect()`` now always disables reconnection and
352
+ stops periodic tasks, and tears down the SDK connection even when not
353
+ connected.
354
+ - **Fix queued commands being lost after active/deep reconnection**: the
355
+ command queue was only flushed from the SDK's ``on_connection_resumed``
356
+ callback, which never fires for the new connection built by
357
+ active/deep reconnection. Commands queued while offline were silently
358
+ dropped. Both reconnect paths now flush the queue after subscriptions
359
+ are restored.
360
+ - **Fix periodic request tasks dying on MQTT errors**: the periodic loop
361
+ caught only ``AwsCrtError`` and ``RuntimeError``;
362
+ ``MqttNotConnectedError``/``MqttPublishError`` raised by a publish
363
+ racing a disconnection permanently killed the polling task while it
364
+ still appeared active. The loop now survives all library errors.
365
+ - **Fix silent failures in thread-scheduled coroutines**: futures
366
+ returned by ``run_coroutine_threadsafe`` were discarded, so exceptions
367
+ from scheduled work (e.g. a failed resubscribe after a clean-session
368
+ resume, leaving the client connected but deaf) vanished. A done
369
+ callback now logs them.
370
+ - **Fix CancelledError being swallowed in reconnection and periodic
371
+ loops**: both loops caught ``asyncio.CancelledError`` and ``break``-ed,
372
+ so cancelled tasks ended "successfully" (and the reconnection loop
373
+ could emit ``reconnection_failed`` during a manual disconnect).
374
+ Cancellation now propagates correctly.
375
+ - **Fix AttributeError in configure_reservation_water_program**: The
376
+ ``NavienMqttClient`` proxy referenced ``self._control``, which is never
377
+ assigned (the attribute is ``_device_controller``), so every call raised
378
+ ``AttributeError``. Now delegates correctly; a regression test guards all
379
+ proxies against references to the undefined attribute.
380
+ - **Fix broken CLI mode choices**: ``nwp-cli mode vacation`` always failed
381
+ because vacation mode (5) requires a day count that was never supplied, and
382
+ ``mode standby`` sent the invalid writable mode value ``0``. Both choices
383
+ were removed from the ``mode`` command; use the dedicated ``vacation DAYS``
384
+ and ``power off`` commands instead.
385
+ - **Fix CLI exit codes**: click ignores command return values in standalone
386
+ mode, so the CLI always exited ``0`` even when a command failed. Failures
387
+ now propagate through ``ctx.exit()`` and produce a non-zero exit code for
388
+ scripts and automation.
389
+ - **Fix cached tokens being reused for a different account**: passing
390
+ ``--email`` for account B while tokens for account A were cached silently
391
+ ran commands against account A's session. Cached tokens are now discarded
392
+ when the provided email does not match the cached one.
393
+ - **Surface OpenEI application errors**: OpenEI reports errors such as an
394
+ invalid API key in the body of an HTTP 200 response; these were masked as
395
+ "no rate plans found". ``fetch_rates()`` now raises ``APIError`` with the
396
+ API's error message.
397
+ - **Fix broken example import**: ``examples/advanced/mqtt_diagnostics.py``
398
+ imported ``MqttConnectionConfig`` from the nonexistent ``nwp500.mqtt_utils``
399
+ module and used the deprecated ``datetime.utcnow()``.
400
+
401
+ Improvements
402
+ ------------
403
+ - **MQTT operation acknowledgement timeouts**: connect, publish,
404
+ subscribe, unsubscribe, and disconnect acknowledgements are now awaited
405
+ with a timeout (``MqttConnectionConfig.operation_timeout``, default 30
406
+ seconds). Previously a half-open TCP connection could hang callers
407
+ until the 20-minute keep-alive expired.
408
+ - **Reconnection backoff jitter**: reconnect delays are now randomized
409
+ (±50%, capped at ``max_reconnect_delay``) so fleets of clients
410
+ disconnected simultaneously (e.g. the AWS IoT 24-hour disconnect) no
411
+ longer reconnect in synchronized waves.
412
+
413
+ Security
414
+ --------
415
+ - **Restrict token cache file permissions**: ``~/.nwp500_tokens.json``
416
+ (refresh token and AWS credentials) was written world-readable. It is now
417
+ created with mode ``0600``, and existing files are tightened on save.
418
+
8
419
  Version 8.1.3 (2026-06-15)
9
420
  ==========================
10
421
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.3
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
@@ -418,7 +418,7 @@ Encodes a floating-point price into an integer for transmission.
418
418
 
419
419
  .. code-block:: python
420
420
 
421
- from nwp500 import encode_price
421
+ from nwp500.encoding import encode_price
422
422
 
423
423
  # Encode $0.45000 per kWh
424
424
  encoded = encode_price(0.45, decimal_point=5)
@@ -437,7 +437,7 @@ Decodes an integer price back to floating-point.
437
437
 
438
438
  .. code-block:: python
439
439
 
440
- from nwp500 import decode_price
440
+ from nwp500.encoding import decode_price
441
441
 
442
442
  # Decode price from device
443
443
  price = decode_price(45000, decimal_point=5)
@@ -466,7 +466,7 @@ Encodes a list of day names into a bitfield.
466
466
 
467
467
  .. code-block:: python
468
468
 
469
- from nwp500 import encode_week_bitfield
469
+ from nwp500.encoding import encode_week_bitfield
470
470
 
471
471
  # Weekdays only
472
472
  bitfield = encode_week_bitfield([
@@ -487,7 +487,7 @@ Decodes a bitfield back into a list of day names.
487
487
 
488
488
  .. code-block:: python
489
489
 
490
- from nwp500 import decode_week_bitfield
490
+ from nwp500.encoding import decode_week_bitfield
491
491
 
492
492
  # Decode weekday bitfield
493
493
  days = decode_week_bitfield(62)
@@ -504,7 +504,8 @@ Configure two rate periods - off-peak and peak pricing:
504
504
  .. code-block:: python
505
505
 
506
506
  import asyncio
507
- from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period
507
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
508
+ from nwp500.encoding import build_tou_period
508
509
 
509
510
  async def configure_simple_tou():
510
511
  async with NavienAuthClient("user@example.com", "password") as auth_client:
@@ -654,7 +655,7 @@ Query the device for its current TOU configuration:
654
655
 
655
656
  .. code-block:: python
656
657
 
657
- from nwp500 import decode_week_bitfield, decode_price
658
+ from nwp500.encoding import decode_week_bitfield, decode_price
658
659
 
659
660
  async def check_tou_settings():
660
661
  async with NavienAuthClient("user@example.com", "password") as auth_client:
@@ -64,8 +64,8 @@ Quick Example
64
64
  NavienAuthClient,
65
65
  NavienAPIClient,
66
66
  NavienMqttClient,
67
- build_reservation_entry,
68
67
  )
68
+ from nwp500.encoding import build_reservation_entry
69
69
 
70
70
  async def main():
71
71
  async with NavienAuthClient(
@@ -279,7 +279,7 @@ Helper Functions
279
279
 
280
280
  .. code-block:: python
281
281
 
282
- from nwp500 import build_reservation_entry
282
+ from nwp500.encoding import build_reservation_entry
283
283
 
284
284
  entry = build_reservation_entry(
285
285
  enabled=True,
@@ -589,8 +589,8 @@ want to send the whole weekly program as one typed object.
589
589
  from nwp500 import (
590
590
  WeeklyReservationEntry,
591
591
  WeeklyReservationSchedule,
592
- build_reservation_entry,
593
592
  )
593
+ from nwp500.encoding import build_reservation_entry
594
594
 
595
595
  morning = WeeklyReservationEntry.model_validate(
596
596
  build_reservation_entry(
@@ -529,7 +529,6 @@ Error Handling
529
529
 
530
530
  from nwp500 import (
531
531
  InvalidCredentialsError,
532
- TokenExpiredError,
533
532
  TokenRefreshError,
534
533
  AuthenticationError
535
534
  )
@@ -543,9 +542,6 @@ Error Handling
543
542
  except InvalidCredentialsError:
544
543
  print("Wrong email or password")
545
544
 
546
- except TokenExpiredError:
547
- print("Token expired and refresh failed")
548
-
549
545
  except TokenRefreshError:
550
546
  print("Could not refresh token - sign in again")
551
547
 
@@ -224,12 +224,6 @@ Set operation mode.
224
224
  # High Demand (maximum capacity)
225
225
  python3 -m nwp500.cli mode high-demand
226
226
 
227
- # Vacation Mode
228
- python3 -m nwp500.cli mode vacation
229
-
230
- # Standby
231
- python3 -m nwp500.cli mode standby
232
-
233
227
  **Syntax:**
234
228
 
235
229
  .. code-block:: bash
@@ -238,12 +232,13 @@ Set operation mode.
238
232
 
239
233
  **Available Modes:**
240
234
 
241
- * ``standby`` - Device off but ready
242
- * ``heat-pump`` - Heat pump only (0)
235
+ * ``heat-pump`` - Heat pump only (1)
243
236
  * ``electric`` - Electric heating only (2)
244
237
  * ``energy-saver`` - Hybrid/balanced mode (3) **recommended**
245
238
  * ``high-demand`` - Maximum heating capacity (4)
246
- * ``vacation`` - Extended vacancy mode (5)
239
+
240
+ For vacation mode use the ``vacation`` command (it requires a day count);
241
+ to power the unit off use the ``power`` command.
247
242
 
248
243
  **Output:** Confirmation message and updated device status.
249
244
 
@@ -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