nwp500-python 9.3.1__tar.gz → 9.4.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 (231) hide show
  1. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/workflows/ci.yml +33 -3
  2. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/CHANGELOG.rst +184 -0
  3. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/Makefile +4 -3
  4. {nwp500_python-9.3.1/src/nwp500_python.egg-info → nwp500_python-9.4.0}/PKG-INFO +1 -1
  5. nwp500_python-9.4.0/RELEASE.md +418 -0
  6. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/index.rst +1 -0
  7. nwp500_python-9.4.0/docs/explanation/tou-recovery-cap.rst +188 -0
  8. nwp500_python-9.4.0/docs/how-to/maintenance.rst +139 -0
  9. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/schedule-operation.rst +36 -69
  10. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/track-energy.rst +43 -15
  11. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/openapi.yaml +15 -29
  12. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/data_conversions.rst +2 -2
  13. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/device_status.rst +2 -2
  14. nwp500_python-9.4.0/docs/reference/protocol/mqtt_protocol.rst +1230 -0
  15. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/quick_reference.rst +61 -29
  16. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/cli.rst +133 -24
  17. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/events.rst +3 -1
  18. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/models.rst +101 -62
  19. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/mqtt_client.rst +174 -66
  20. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/error_code_demo.py +3 -1
  21. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/bump_version.py +4 -5
  22. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/__init__.py +12 -4
  23. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/__main__.py +205 -16
  24. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/handlers.py +283 -1
  25. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/output_formatters.py +62 -0
  26. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/presentation.py +251 -8
  27. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/rich_output.py +55 -10
  28. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/converters.py +1 -1
  29. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/enums.py +40 -16
  30. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/__init__.py +13 -4
  31. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/device.py +8 -5
  32. nwp500_python-9.4.0/src/nwp500/models/diagnostics.py +147 -0
  33. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/energy.py +35 -3
  34. nwp500_python-9.4.0/src/nwp500/models/firmware.py +47 -0
  35. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/schedule.py +105 -157
  36. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/status.py +13 -1
  37. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/_control_commands.py +42 -33
  38. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/_device_subscriptions.py +105 -17
  39. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/client.py +76 -3
  40. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/control.py +435 -157
  41. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/subscriptions.py +327 -157
  42. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/utils.py +9 -0
  43. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/reservations.py +3 -1
  44. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/topic_builder.py +49 -6
  45. {nwp500_python-9.3.1 → nwp500_python-9.4.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  46. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/SOURCES.txt +5 -0
  47. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/scm_file_list.json +6 -1
  48. nwp500_python-9.4.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  49. nwp500_python-9.4.0/tests/test_app_queries.py +901 -0
  50. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_commands.py +434 -1
  51. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_rest_models.py +19 -0
  52. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_protocol_correctness.py +353 -132
  53. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_public_api.py +59 -0
  54. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_reservations.py +21 -0
  55. nwp500_python-9.4.0/uv.lock +3 -0
  56. nwp500_python-9.3.1/RELEASE.md +0 -393
  57. nwp500_python-9.3.1/docs/how-to/maintenance.rst +0 -119
  58. nwp500_python-9.3.1/docs/reference/protocol/mqtt_protocol.rst +0 -929
  59. nwp500_python-9.3.1/src/nwp500_python.egg-info/scm_version.json +0 -8
  60. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.coveragerc +0 -0
  61. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  62. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/copilot-instructions.md +0 -0
  63. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.github/workflows/release.yml +0 -0
  64. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.gitignore +0 -0
  65. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.pre-commit-config.yaml +0 -0
  66. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/.readthedocs.yml +0 -0
  67. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/AUTHORS.rst +0 -0
  68. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/CONTRIBUTING.rst +0 -0
  69. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/LICENSE.txt +0 -0
  70. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/README.rst +0 -0
  71. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/Makefile +0 -0
  72. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/_static/.gitignore +0 -0
  73. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/conf.py +0 -0
  74. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/advanced-features.rst +0 -0
  75. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/architecture.rst +0 -0
  76. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/tank-energy.rst +0 -0
  77. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/explanation/unknown-values.rst +0 -0
  78. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/authenticate.rst +0 -0
  79. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/auto-recovery.rst +0 -0
  80. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  81. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/home-assistant.rst +0 -0
  82. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/index.rst +0 -0
  83. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/manage-units.rst +0 -0
  84. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/monitor-status.rst +0 -0
  85. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/optimize-tou.rst +0 -0
  86. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/how-to/queue-commands.rst +0 -0
  87. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/index.rst +0 -0
  88. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/authors.rst +0 -0
  89. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/changelog.rst +0 -0
  90. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/contributing.rst +0 -0
  91. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/history.rst +0 -0
  92. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/project/license.rst +0 -0
  93. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/configuration.rst +0 -0
  94. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/enumerations.rst +0 -0
  95. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/index.rst +0 -0
  96. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/installation.rst +0 -0
  97. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/device_features.rst +0 -0
  98. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/error_codes.rst +0 -0
  99. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/protocol/rest_api.rst +0 -0
  100. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/api_client.rst +0 -0
  101. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/auth_client.rst +0 -0
  102. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/reference/python_api/exceptions.rst +0 -0
  103. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/requirements.txt +0 -0
  104. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/docs/tutorials/getting-started.rst +0 -0
  105. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/.ruff.toml +0 -0
  106. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/README.md +0 -0
  107. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/air_filter_reset.py +0 -0
  108. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/anti_legionella.py +0 -0
  109. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/auto_recovery.py +0 -0
  110. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/combined_callbacks.py +0 -0
  111. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/demand_response.py +0 -0
  112. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/device_capabilities.py +0 -0
  113. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/device_status_debug.py +0 -0
  114. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/energy_analytics.py +0 -0
  115. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/firmware_payload_capture.py +0 -0
  116. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  117. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/power_control.py +0 -0
  118. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/recirculation_control.py +0 -0
  119. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/reconnection_demo.py +0 -0
  120. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/reservation_schedule.py +0 -0
  121. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/simple_auto_recovery.py +0 -0
  122. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/token_restoration.py +0 -0
  123. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/tou_openei.py +0 -0
  124. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/tou_schedule.py +0 -0
  125. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/advanced/water_reservation.py +0 -0
  126. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/01_authentication.py +0 -0
  127. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/02_list_devices.py +0 -0
  128. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/03_get_status.py +0 -0
  129. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/beginner/04_set_temperature.py +0 -0
  130. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  131. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/command_queue.py +0 -0
  132. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/device_status_callback.py +0 -0
  133. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/error_handling.py +0 -0
  134. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/event_driven_control.py +0 -0
  135. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/improved_auth.py +0 -0
  136. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  137. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  138. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/periodic_requests.py +0 -0
  139. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/set_mode.py +0 -0
  140. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/intermediate/vacation_mode.py +0 -0
  141. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/mask.py +0 -0
  142. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/periodic_device_info.py +0 -0
  143. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/simple_periodic_info.py +0 -0
  144. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_api_client.py +0 -0
  145. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_mqtt_connection.py +0 -0
  146. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_mqtt_messaging.py +0 -0
  147. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/examples/testing/test_periodic_minimal.py +0 -0
  148. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/pyproject.toml +0 -0
  149. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/README.md +0 -0
  150. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/diagnose_mqtt_connection.py +0 -0
  151. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/extract_changelog.py +0 -0
  152. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/format.py +0 -0
  153. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/lint.py +0 -0
  154. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/setup-dev.py +0 -0
  155. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/scripts/validate_version.py +0 -0
  156. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/setup.cfg +0 -0
  157. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/setup.py +0 -0
  158. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/_base.py +0 -0
  159. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/api_client.py +0 -0
  160. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/auth.py +0 -0
  161. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/__init__.py +0 -0
  162. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/monitoring.py +0 -0
  163. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/cli/token_storage.py +0 -0
  164. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/command_decorators.py +0 -0
  165. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/config.py +0 -0
  166. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/device_capabilities.py +0 -0
  167. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/device_info_cache.py +0 -0
  168. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/encoding.py +0 -0
  169. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/events.py +0 -0
  170. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/exceptions.py +0 -0
  171. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/factory.py +0 -0
  172. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/field_factory.py +0 -0
  173. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/_converters.py +0 -0
  174. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/feature.py +0 -0
  175. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/mqtt_models.py +0 -0
  176. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/models/tou.py +0 -0
  177. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/__init__.py +0 -0
  178. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/command_queue.py +0 -0
  179. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/connection.py +0 -0
  180. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  181. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/periodic.py +0 -0
  182. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/reconnection.py +0 -0
  183. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  184. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt/types.py +0 -0
  185. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/mqtt_events.py +0 -0
  186. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/openei.py +0 -0
  187. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/py.typed +0 -0
  188. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/temperature.py +0 -0
  189. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/tou_schedule.py +0 -0
  190. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/unit_system.py +0 -0
  191. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500/utils.py +0 -0
  192. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  193. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  194. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  195. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  196. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  197. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/conftest.py +0 -0
  198. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_api_helpers.py +0 -0
  199. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_auth.py +0 -0
  200. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_auth_session_lifecycle.py +0 -0
  201. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_bug_fixes.py +0 -0
  202. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_canonical_schedule.py +0 -0
  203. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_basic.py +0 -0
  204. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_cli_tristate_presentation.py +0 -0
  205. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_command_decorators.py +0 -0
  206. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_command_queue.py +0 -0
  207. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_capabilities.py +0 -0
  208. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_device_info_cache.py +0 -0
  209. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_events.py +0 -0
  210. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_exceptions.py +0 -0
  211. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_model_converters.py +0 -0
  212. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_models.py +0 -0
  213. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  214. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_client_init.py +0 -0
  215. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_events.py +0 -0
  216. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_hypothesis.py +0 -0
  217. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection.py +0 -0
  218. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  219. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_mqtt_reliability.py +0 -0
  220. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_multi_device.py +0 -0
  221. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_openei.py +0 -0
  222. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_temperature_converters.py +0 -0
  223. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_threading_model.py +0 -0
  224. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_token_storage.py +0 -0
  225. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_tou_api.py +0 -0
  226. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_tou_schedule.py +0 -0
  227. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_unit_switching.py +0 -0
  228. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_unit_system_process_wide.py +0 -0
  229. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_utility_modules.py +0 -0
  230. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tests/test_utils.py +0 -0
  231. {nwp500_python-9.3.1 → nwp500_python-9.4.0}/tox.ini +0 -0
@@ -69,15 +69,45 @@ jobs:
69
69
 
70
70
  - name: Run tox tests
71
71
  run: tox -e default
72
- - name: Install project testing extras (ensure pytest-asyncio present)
72
+
73
+ test-without-cli-extras:
74
+ # tox installs the cli extra, so the job above never exercises an
75
+ # install without click or rich. release.yml installs
76
+ # -e ".[testing]", which has neither, and a test module importing
77
+ # click above its ImportError guard failed there and nowhere else,
78
+ # after the release tag was already pushed. This job runs the
79
+ # release workflow's install so that failure surfaces on the pull
80
+ # request instead.
81
+ name: Test without CLI extras
82
+ runs-on: ubuntu-latest
83
+
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+
87
+ - name: Set up Python
88
+ uses: actions/setup-python@v5
89
+ with:
90
+ python-version: '3.14'
91
+
92
+ - name: Install project with testing extras only
73
93
  run: |
74
94
  python -m pip install --upgrade pip
75
- python -m pip install '.[testing]'
95
+ python -m pip install -e ".[testing]"
96
+
97
+ - name: Verify the CLI extras really are absent
98
+ run: |
99
+ if python -c 'import click' 2>/dev/null; then
100
+ echo "click is installed; this job no longer tests what it claims to"
101
+ exit 1
102
+ fi
103
+
104
+ - name: Run tests
105
+ run: python -m pytest
76
106
 
77
107
  build:
78
108
  name: Build Distribution
79
109
  runs-on: ubuntu-latest
80
- needs: [lint, test]
110
+ needs: [lint, test, test-without-cli-extras]
81
111
 
82
112
  steps:
83
113
  - uses: actions/checkout@v4
@@ -5,6 +5,189 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.4.0 (2026-09-12)
9
+ ==========================
10
+
11
+ This release realigns the MQTT command set with what the NaviLink app
12
+ actually sends. The app (version 2.03.00) was decompiled and its request
13
+ builder compared with the library; every query it sends now has a
14
+ method, six commands it never sends were removed, and the recirculation
15
+ schedule write was corrected. Breaking changes are listed under
16
+ *Removed* and *Changed*.
17
+
18
+ Added
19
+ -----
20
+ - **Installer diagnostics query.** ``request_diagnostics()`` /
21
+ ``subscribe_diagnostics()`` and the ``nwp-cli diagnostics`` command
22
+ return the lifetime counters the app's installer screen shows: heat
23
+ pump and element energy, days since installation, fault event counts,
24
+ demand-response operation times, hot-water draw statistics and
25
+ component run times and start counts, as
26
+ :class:`~nwp500.models.DeviceDiagnostics`. The device answers the
27
+ ``st/td/rd`` query with packed hex; the NaviLink cloud decodes it into
28
+ JSON only when the reply topic has the app's five-segment form, so the
29
+ library requests that form. Verified live: the energies and compressor
30
+ and element run times match the energy query's lifetime totals.
31
+ - **Monthly energy query.** ``request_energy_usage_monthly()`` /
32
+ ``subscribe_energy_usage_monthly()`` and ``nwp-cli energy --years``
33
+ return one entry per year with twelve per-month items. Verified live.
34
+ - **Hourly energy query.** ``request_energy_usage_hourly()`` /
35
+ ``subscribe_energy_usage_hourly()`` send the app's per-hour query. The
36
+ NWP500 tested never answered it in four attempts, so it has no CLI
37
+ command.
38
+ - **Recirculation schedule read.** ``request_recirculation_schedule()``
39
+ and ``nwp-cli recirc-schedule get``; the write is exposed as
40
+ ``nwp-cli recirc-schedule set``, which checks the JSON before
41
+ connecting (known keys only, integers in range, at most 20 entries).
42
+ The read was verified live on a unit that reports recirculation
43
+ scheduling but has no pump (empty, disabled schedule); the write and
44
+ its echo are unverified. The CLI reports a write as updated only when
45
+ the echo matches what was written. ``RecirculationSchedule`` and
46
+ ``RecirculationScheduleEntry`` fields are strict integers, so ``True``
47
+ or ``6.0`` is rejected instead of coerced.
48
+ - **Firmware download info.** ``request_firmware_download_info()`` /
49
+ ``subscribe_firmware_download_info()`` and ``nwp-cli firmware info``
50
+ return :class:`~nwp500.models.FirmwareDownloadInfo`. The device replies
51
+ on its own topic rather than a client-keyed one. Verified live.
52
+ - **Session end.** ``end_session()`` sends the ``st/end`` query the app
53
+ sends whenever it leaves a device. No reply to it has been observed.
54
+ Unlike other commands it is never queued while disconnected (it raises
55
+ ``MqttNotConnectedError``), since a session end replayed after a
56
+ reconnect would end the new session.
57
+ - ``MqttConnectionConfig.send_session_end_on_disconnect`` (default
58
+ ``True``) controls whether ``disconnect()`` sends it.
59
+ - ``subscribe_firmware_commit_response()`` receives the reply to
60
+ ``commit_firmware_update()``, which is requested on the app-form
61
+ ``res/commit-ota`` topic that no other subscription covers.
62
+ - **Controls with app builder cases that had no method:**
63
+ ``set_vacation_duration(days)`` (``goout-day``; the app has a builder
64
+ case but no screen that calls it; verified live to change the day count
65
+ without changing the operation mode),
66
+ ``set_air_filter_life(hours)`` (``air-filter-life``; the wire value is
67
+ hours divided by 500, matching the app's 0 or 1000-10000 picker;
68
+ verified live) and ``reset_condenser_fault()`` (``cond-fault-reset``,
69
+ installer-level in the app; acknowledged live, clearing effect not
70
+ observable without a fault). CLI: ``vacation-duration``,
71
+ ``filter-life``, ``reset-condenser-fault``.
72
+ - **Command codes** for every member of the app's ``DeviceControlMGPP``
73
+ enum, with the declared-but-never-sent ones annotated.
74
+ - ``EnergyUsageResponse.type_of_usage``, ``get_year_data()``,
75
+ ``MonthlyEnergyData.day`` and ``EnergyUsageDay.ep_usage`` /
76
+ ``water_usage`` (declared by the app; not reported by the NWP500).
77
+
78
+ Changed
79
+ -------
80
+ - **Recirculation schedule models take the app's entry shape** (breaking).
81
+ :class:`~nwp500.models.RecirculationScheduleEntry` is now
82
+ ``enable``/``week``/``hour``/``min``/``mode``/``param`` like a
83
+ reservation entry (``mode`` is the entry's pump on/off toggle in the
84
+ app's editor) and :class:`~nwp500.models.RecirculationSchedule` carries
85
+ ``reservation_use`` plus ``reservation`` instead of ``schedule``. The
86
+ previous start/end window fields were not what the device accepts.
87
+ - ``MonthlyEnergyData.month`` is optional: monthly-query entries have no
88
+ month.
89
+ - **``NavienMqttClient.disconnect()`` now sends ``st/end``** to every
90
+ device that was passed to any subscription, as the NaviLink app does
91
+ when it leaves a device. The publishes go straight to the connection,
92
+ never into the offline command queue, stop if the connection drops,
93
+ are bounded by a short timeout and never fail the disconnect. What it
94
+ does to other clients connected to the same device (for example a Home
95
+ Assistant integration) is not known; turn it off with
96
+ ``MqttConnectionConfig(send_session_end_on_disconnect=False)``.
97
+ - **CLI energy summaries total the requested periods.** ``energy
98
+ --month``, ``--months`` and ``--years`` printed the device's lifetime
99
+ total under a per-month or per-year heading. Each summary now covers
100
+ only the rows shown, and the lifetime total is printed once under its
101
+ own heading. (The response's ``total`` is lifetime whatever is
102
+ requested; checked live.)
103
+ - **Parameter validation.** ``set_air_filter_life()``,
104
+ ``set_vacation_duration()`` and the daily, monthly and hourly energy
105
+ queries reject non-integers (including ``True`` and floats) and
106
+ out-of-range values; years must be 2000-2099 and ``--years`` drops
107
+ duplicates. ``filter-life``, ``--year`` and ``--years`` reject bad
108
+ input before connecting. ``request_energy_usage()`` now rejects an
109
+ empty month list.
110
+
111
+ Fixed
112
+ -----
113
+ - **``configure_recirculation_schedule()`` published to the wrong topic
114
+ with the wrong keys.** It sent ``{"schedule": [...]}`` on the bare
115
+ ``ctrl`` topic; the app publishes RECIR_RESERVATION on
116
+ ``ctrl/recirc-rsv/rd`` with ``reservationUse`` and ``reservation`` and
117
+ asks for the echo on ``recirc-rsv/rd``.
118
+ - **``commit_firmware_update()`` published to the bare ``ctrl`` topic.**
119
+ The app publishes OTA_COMMIT on ``ctrl/commit-ota`` and asks for the
120
+ reply on ``res/commit-ota``; the library now does the same. Not
121
+ exercised against a device.
122
+ - **Unsubscribing could remove the wrong handler.** When one callback was
123
+ registered for more than one device-wildcard subscription (status,
124
+ feature, firmware info), unsubscribing one removed whichever handler
125
+ came first. Handlers are now matched on subscription kind as well.
126
+ - **Query replies could reach another device's callback.** Client-keyed
127
+ reply topics are shared by every device a client queries, so with two
128
+ devices subscribed a reservation, energy, TOU, diagnostics or
129
+ recirculation reply reached both callbacks. Typed subscriptions now
130
+ ignore replies whose ``macAddress`` names a different device.
131
+ - Recirculation read-backs in hex carry the unused ``param`` as ``0xFF``
132
+ and in JSON as ``-1``; the model normalizes to ``-1`` so the two compare
133
+ equal.
134
+ - **Protocol documentation** rewritten from the decompiled request
135
+ builder and live captures: real topic structure, the anti-legionella
136
+ modes (``anti-leg-on [days]`` / ``anti-leg-off``, not
137
+ ``anti-legionella-setting``), the message example that paired the
138
+ weekly-reservation code with ``dhw-temperature``, the undocumented
139
+ 16777226 reservation write, response shapes for every query, and the
140
+ quick reference's invented ``header``/``body`` format with 0x11/0x21/0x31
141
+ codes that never existed. Stale ``docs/protocol/`` paths in code
142
+ comments and docstrings now point at ``docs/reference/protocol/``.
143
+ - ``models/schedule.py`` docstrings cited 33554444 (the hot button) for
144
+ the recirculation reservation; it is 33554440.
145
+ - CLI reference: token cache path is ``~/.nwp500_tokens.json``, ``-vv``
146
+ is a repeated ``-v`` (there was no ``--very-verbose``), and the
147
+ ``--unit-system`` option and ``tou rates``/``plan``/``apply`` commands
148
+ are documented.
149
+
150
+ Removed
151
+ -------
152
+ - **Six MQTT commands the NaviLink app never sends** (breaking):
153
+ ``check_firmware_update()``, ``reconnect_wifi()``, ``reset_wifi()``,
154
+ ``set_freeze_protection_temperature()``, ``run_smart_diagnostic()`` and
155
+ ``update_weekly_reservation()`` with
156
+ ``subscribe_weekly_reservation_response()`` and the
157
+ ``WeeklyReservationEntry`` / ``WeeklyReservationSchedule`` models. The
158
+ app's enum declares the first five codes but its request builder has
159
+ no case for them, so the library's ``ota-check``, ``wifi-reconnect``,
160
+ ``wifi-reset``, ``frez-temp`` and ``smart-diagnostic`` mode strings were
161
+ invented. The app's weekly schedule is the ordinary reservation
162
+ program: it writes with 16777226 on ``ctrl/rsv/rd``
163
+ (``update_reservations()``), never with 33554438 or a ``rsv-weekly/rd``
164
+ topic. The ``CommandCode`` members stay, annotated as declared only.
165
+
166
+ Version 9.3.2 (2026-09-01)
167
+ ==========================
168
+
169
+ Fixed
170
+ -----
171
+ - **A null ``errorCode`` no longer makes ``/device/list`` unparseable.**
172
+ The cloud returns ``"error": {"errorCode": null}`` on some devices.
173
+ :class:`~nwp500.models.DeviceErrorSummary` typed the field
174
+ ``ErrorCode | int``, so validation of the whole listing failed and every
175
+ caller - including the Home Assistant integration, which could no longer
176
+ set up - lost the device entirely. ``error_code`` is now
177
+ ``ErrorCode | int | None`` and defaults to ``None``: a null means the
178
+ cloud reported no code, which is not the same claim as ``NO_ERROR``.
179
+ Callers that treated the field as always-present should handle ``None``.
180
+
181
+ Security
182
+ --------
183
+ - **MAC address no longer logged in the clear when a reservations
184
+ unsubscribe fails.** ``fetch_reservations`` logged
185
+ ``device_info.mac_address`` unredacted in its cleanup warning, while the
186
+ identical warning in ``update_reservations_confirmed`` a few lines below
187
+ passed it through ``redact_mac``. CodeQL flagged it as
188
+ ``py/clear-text-logging-sensitive-data`` (high). Both call sites now
189
+ redact.
190
+
8
191
  Version 9.3.1 (2026-08-29)
9
192
  ==========================
10
193
 
@@ -104,6 +287,7 @@ Fixed
104
287
  other clients on the same device. Query results the device routes back to a
105
288
  client-keyed topic, such as reservations and energy usage, were already
106
289
  covered by the existing response wildcard.
290
+
107
291
  Version 9.3.0 (2026-08-03)
108
292
  ==========================
109
293
 
@@ -53,10 +53,11 @@ build: clean ## Build distribution packages
53
53
  check-release: lint format-check test validate-version ## Run all checks before release (lint, format check, tests, version validation)
54
54
  @echo "✓ All checks passed! Ready for release."
55
55
 
56
- release: check-release build ## Prepare and build a release (run checks, then build)
56
+ release: check-release build ## Run the release checks and build locally (does not publish)
57
57
  @echo "✓ Release build complete!"
58
- @echo "To publish to TestPyPI: make publish-test"
59
- @echo "To publish to PyPI: make publish"
58
+ @echo "This built locally and published nothing."
59
+ @echo "To release: make version-bump BUMP=patch, then push main and the tag."
60
+ @echo "Pushing the tag is what publishes to PyPI. See RELEASE.md."
60
61
 
61
62
  publish-test: ## Publish to TestPyPI
62
63
  python -m twine check dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.3.1
3
+ Version: 9.4.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
@@ -0,0 +1,418 @@
1
+ # Release Process
2
+
3
+ This document describes the release process for nwp500-python, including code quality checks, formatting, and publishing.
4
+
5
+ ## Prerequisites
6
+
7
+ Install development dependencies:
8
+
9
+ ```bash
10
+ pip install -e ".[dev]"
11
+ # or
12
+ make install-dev
13
+ ```
14
+
15
+ ## Quick Release
16
+
17
+ Despite the name, this does not release anything - it runs the checks and
18
+ builds locally so you can see the release is in a fit state:
19
+
20
+ ```bash
21
+ make release
22
+ ```
23
+
24
+ This will:
25
+ 1. Run linting checks
26
+ 2. Verify code formatting
27
+ 3. Run all tests
28
+ 4. Validate the version configuration
29
+ 5. Clean build artifacts
30
+ 6. Build distribution packages
31
+
32
+ Releasing is `make version-bump` followed by pushing the tag, which hands off
33
+ to CI. See [The Release Runs Itself](#5-the-release-runs-itself).
34
+
35
+ ## Step-by-Step Release Process
36
+
37
+ ### 1. Code Quality Checks
38
+
39
+ #### Format Code
40
+
41
+ Format all code with ruff:
42
+
43
+ ```bash
44
+ make format
45
+ # or
46
+ tox -e format
47
+ ```
48
+
49
+ This will:
50
+ - Automatically fix linting issues where possible
51
+ - Format code to comply with PEP 8 and project standards
52
+ - Sort imports according to isort rules
53
+
54
+ #### Check Linting
55
+
56
+ Check code without making changes:
57
+
58
+ ```bash
59
+ make lint
60
+ # or
61
+ tox -e lint
62
+ ```
63
+
64
+ #### Verify Formatting
65
+
66
+ Check that code is properly formatted:
67
+
68
+ ```bash
69
+ make format-check
70
+ ```
71
+
72
+ ### 2. Run Tests
73
+
74
+ Run the test suite:
75
+
76
+ ```bash
77
+ make test
78
+ # or
79
+ pytest
80
+ ```
81
+
82
+ Run tests with coverage report:
83
+
84
+ ```bash
85
+ make test-cov
86
+ ```
87
+
88
+ ### 3. Run All Checks
89
+
90
+ Run all quality checks at once:
91
+
92
+ ```bash
93
+ make check-release
94
+ ```
95
+
96
+ This runs:
97
+ - Linting checks
98
+ - Format verification
99
+ - Full test suite
100
+
101
+ ### 4. Update Version and Changelog
102
+
103
+ #### Understanding Version Management
104
+
105
+ **IMPORTANT**: This project uses `setuptools_scm` to manage versions from git tags.
106
+ The version is **NOT** stored in any Python files or config files.
107
+
108
+ **DO NOT** edit the `version` field in `setup.cfg`'s `[pyscaffold]` section!
109
+ That field stores the PyScaffold tool version (4.6), not the package version.
110
+
111
+ #### Version Bump Process
112
+
113
+ The bump script owns the version heading, the changelog commit and the tag.
114
+ Do not write any of them by hand - `scripts/extract_changelog.py` parses the
115
+ heading it produces to build the GitHub release notes, and a hand-written
116
+ duplicate silently breaks that.
117
+
118
+ 1. Make sure `CHANGELOG.rst` describes the release under `Unreleased`.
119
+
120
+ Add entries only - no `Version X.Y.Z` heading, no date. Normally each PR
121
+ has already added its own entry, so this is a review rather than a write:
122
+
123
+ ```rst
124
+ Unreleased
125
+ ==========
126
+
127
+ Fixed
128
+ -----
129
+ - **Something was broken.** It is no longer broken.
130
+ ```
131
+
132
+ If you edit anything here, commit it before continuing: the script
133
+ refuses to run on a dirty working directory.
134
+
135
+ 2. Run the bump script:
136
+
137
+ ```bash
138
+ # For a patch release (X.Y.Z -> X.Y.Z+1)
139
+ make version-bump BUMP=patch
140
+
141
+ # For a minor release (X.Y.Z -> X.Y+1.0)
142
+ make version-bump BUMP=minor
143
+
144
+ # For a major release (X.Y.Z -> X+1.0.0)
145
+ make version-bump BUMP=major
146
+
147
+ # Or specify an explicit version
148
+ make version-bump BUMP=3.1.5
149
+ ```
150
+
151
+ The script will:
152
+ - Refuse to run unless the working directory is clean
153
+ - Refuse to run if the branch is behind its upstream (it warns, but
154
+ continues, if the branch is ahead or has no upstream)
155
+ - Get the current version from git tags
156
+ - Calculate the new version and validate the progression (prevents large
157
+ jumps)
158
+ - Insert `Version X.Y.Z (YYYY-MM-DD)` under `Unreleased` in `CHANGELOG.rst`
159
+ - Commit that as `Update changelog for vX.Y.Z`
160
+ - Create the annotated tag `vX.Y.Z`
161
+
162
+ 3. Push the commit, then the tag:
163
+
164
+ ```bash
165
+ git push origin main
166
+ git push origin vX.Y.Z
167
+ ```
168
+
169
+ Push the commit first. The tag triggers the release, and the release notes
170
+ come from the changelog at that commit.
171
+
172
+ #### Manual Version Tagging (Not Recommended)
173
+
174
+ If you need to create a tag manually:
175
+
176
+ ```bash
177
+ git tag -a vX.Y.Z -m "Release version X.Y.Z"
178
+ git push origin vX.Y.Z
179
+ ```
180
+
181
+ **Warning**: Manual tagging bypasses the clean-tree, not-behind-upstream and
182
+ version progression checks, and does not touch the changelog - so unless you
183
+ added the `Version X.Y.Z (YYYY-MM-DD)` heading yourself,
184
+ `scripts/extract_changelog.py` finds no matching section and the GitHub release
185
+ notes fall back to the bare line `Release X.Y.Z`. Use the version bump script
186
+ instead.
187
+
188
+ ### 5. The Release Runs Itself
189
+
190
+ Pushing a `v*` tag starts `.github/workflows/release.yml`, which is the only
191
+ thing that publishes. In order, it:
192
+
193
+ 1. **Pre-release Checks** - `ruff check`, `ruff format --check`, and `pytest`
194
+ against an install of `-e ".[testing]"`
195
+ 2. **Build and Publish to PyPI** - `python -m build`, `twine check`, then
196
+ `pypa/gh-action-pypi-publish` via PyPI trusted publishing (no API tokens
197
+ are stored anywhere)
198
+ 3. **Create GitHub Release** - release notes extracted from the `Version
199
+ X.Y.Z` section of `CHANGELOG.rst`
200
+
201
+ Each job depends on the one before it, so a failing check publishes nothing.
202
+
203
+ Watch it:
204
+
205
+ ```bash
206
+ gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')
207
+ ```
208
+
209
+ Then confirm the result:
210
+
211
+ ```bash
212
+ gh release view vX.Y.Z
213
+ pip index versions nwp500-python
214
+ ```
215
+
216
+ #### Do Not Publish By Hand
217
+
218
+ `make publish` and `make publish-test` still exist as an emergency fallback,
219
+ but a normal release must not use them. The workflow has already uploaded the
220
+ artifacts by the time you could, and PyPI refuses to accept a version twice -
221
+ so running them after a successful release fails, and running them before it
222
+ makes the workflow fail instead. Publishing is CI's job.
223
+
224
+ #### If the Release Fails After You Pushed the Tag
225
+
226
+ The tag is the trigger, so a failing pre-release check leaves a tag on the
227
+ remote pointing at a commit that was never released. Nothing was published -
228
+ the publish job is gated behind the checks - so the version number is still
229
+ free. Fix the problem, then move the tag:
230
+
231
+ ```bash
232
+ # after committing the fix and pushing main
233
+ git tag -d vX.Y.Z
234
+ git push origin :refs/tags/vX.Y.Z
235
+ git tag -a vX.Y.Z -m "Release version X.Y.Z"
236
+ git push origin vX.Y.Z
237
+ ```
238
+
239
+ Confirm first that nothing was published - if the publish job did succeed,
240
+ that version is gone for good and you need a new patch version instead:
241
+
242
+ ```bash
243
+ gh run view <run-id> --json jobs --jq '.jobs[] | "\(.name): \(.conclusion)"'
244
+ pip index versions nwp500-python
245
+ ```
246
+
247
+ ### 6. Building Locally (Optional)
248
+
249
+ Building locally is a sanity check, not part of publishing:
250
+
251
+ ```bash
252
+ make build
253
+ ```
254
+
255
+ This creates:
256
+ - `dist/nwp500_python-X.Y.Z.tar.gz` (source distribution)
257
+ - `dist/nwp500_python-X.Y.Z-py3-none-any.whl` (wheel)
258
+
259
+ The version comes from `git describe`, so building on an untagged commit
260
+ produces a development version like `9.3.1.dev3+g1a2b3c4` - that is expected,
261
+ and it is a quick way to confirm the tag you just created is what
262
+ `setuptools_scm` sees.
263
+
264
+ ## Using Tox
265
+
266
+ You can also use tox directly for all steps:
267
+
268
+ ```bash
269
+ # Run lint checks
270
+ tox -e lint
271
+
272
+ # Format code
273
+ tox -e format
274
+
275
+ # Run tests
276
+ tox
277
+
278
+ # Build package
279
+ tox -e build
280
+
281
+ # Clean artifacts
282
+ tox -e clean
283
+ ```
284
+
285
+ ## Ruff Configuration
286
+
287
+ Ruff is configured in `pyproject.toml` with the following rules:
288
+
289
+ - **Line length**: 88 characters (Black-compatible)
290
+ - **Target version**: Python 3.7+
291
+ - **Enabled rules**:
292
+ - `E`, `W`: pycodestyle errors and warnings
293
+ - `F`: Pyflakes
294
+ - `I`: isort (import sorting)
295
+ - `UP`: pyupgrade (Python version upgrades)
296
+ - `B`: flake8-bugbear (common bugs)
297
+ - `C4`: flake8-comprehensions
298
+ - `SIM`: flake8-simplify
299
+
300
+ ### Checking Specific Files
301
+
302
+ ```bash
303
+ # Check specific file
304
+ ruff check src/nwp500/auth.py
305
+
306
+ # Format specific file
307
+ ruff format src/nwp500/auth.py
308
+
309
+ # Check and fix specific directory
310
+ ruff check --fix src/nwp500/
311
+ ```
312
+
313
+ ## Troubleshooting
314
+
315
+ ### Linting Errors
316
+
317
+ If you encounter linting errors:
318
+
319
+ 1. Try auto-fixing: `make format`
320
+ 2. Review remaining errors: `make lint`
321
+ 3. Manually fix any errors that can't be auto-fixed
322
+ 4. Re-run checks: `make check-release`
323
+
324
+ ### Test Failures
325
+
326
+ If tests fail:
327
+
328
+ 1. Review the test output
329
+ 2. Fix the issues in the code
330
+ 3. Re-run tests: `make test`
331
+ 4. Ensure all tests pass before release
332
+
333
+ ### Build Errors
334
+
335
+ If build fails:
336
+
337
+ 1. Clean build artifacts: `make clean`
338
+ 2. Verify dependencies are installed: `pip install -e ".[dev]"`
339
+ 3. Try building again: `make build`
340
+
341
+ ## Pre-Release Checklist
342
+
343
+ Before releasing, ensure:
344
+
345
+ - [ ] Everything intended for the release is merged to `main`
346
+ - [ ] `CHANGELOG.rst` describes it all under `Unreleased`, with no
347
+ hand-written `Version X.Y.Z` heading
348
+ - [ ] All code is formatted: `make format`
349
+ - [ ] Linting passes: `make lint`
350
+ - [ ] All tests pass: `make test`
351
+ - [ ] Version configuration is valid: `make validate-version`
352
+ - [ ] Documentation is up to date
353
+ - [ ] Examples work correctly
354
+ - [ ] Working directory is clean and `main` is in sync with the remote
355
+ (the bump script enforces both)
356
+ - [ ] Build succeeds: `make build`
357
+
358
+ Then bump, push, and let the workflow publish - see
359
+ [The Release Runs Itself](#5-the-release-runs-itself).
360
+
361
+ ## Publishing Credentials
362
+
363
+ There are none to set up. The release workflow publishes through
364
+ [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/): PyPI is
365
+ configured to trust `release.yml` in this repository, and the job mints a
366
+ short-lived token through the `pypi` GitHub environment using its
367
+ `id-token: write` permission. No API token is stored in the repository, in
368
+ GitHub secrets, or on your machine.
369
+
370
+ `TWINE_USERNAME`/`TWINE_PASSWORD` and `~/.pypirc` are only relevant if you
371
+ ever need the emergency `make publish` fallback, which a normal release must
372
+ not use.
373
+
374
+ ## Continuous Integration
375
+
376
+ Two workflows already cover this:
377
+
378
+ **`.github/workflows/ci.yml`** - on pushes and pull requests:
379
+
380
+ | Job | What it does |
381
+ |-----|--------------|
382
+ | Lint and Format Check | `tox -e lint` (`ruff check` and `ruff format --check`) |
383
+ | Security Check | `ruff check --select S src/` |
384
+ | Test on Python 3.14 | `tox -e default` - pytest and pyright, with the `cli` and `testing` extras |
385
+ | Test without CLI extras | pytest against an `-e ".[testing]"` install, matching what `release.yml` does |
386
+ | Build Distribution | `python -m build` plus `twine check` |
387
+
388
+ **`.github/workflows/release.yml`** - on a `v*` tag: pre-release checks, then
389
+ build and publish to PyPI, then create the GitHub release.
390
+
391
+ `Test without CLI extras` exists because `tox` installs the `cli` extra while
392
+ `release.yml` does not. Without it, a test module that imports `click` outside
393
+ its `ImportError` guard passes every pull request and fails only once the
394
+ release tag has been pushed.
395
+
396
+ ## Quick Commands Reference
397
+
398
+ | Command | Description |
399
+ |---------|-------------|
400
+ | `make version-bump` | Bump version (requires BUMP=patch/minor/major/X.Y.Z) |
401
+ | `make help` | Show all available commands |
402
+ | `make install-dev` | Install with dev dependencies |
403
+ | `make format` | Format code with ruff |
404
+ | `make lint` | Check code with ruff |
405
+ | `make test` | Run tests |
406
+ | `make check-release` | Run all pre-release checks |
407
+ | `make release` | Full release build process |
408
+ | `make build` | Build distribution packages locally (CI builds what is published) |
409
+ | `make clean` | Remove build artifacts |
410
+ | `make publish-test` | Emergency fallback only - upload to TestPyPI by hand |
411
+ | `make publish` | Emergency fallback only - upload to PyPI by hand |
412
+
413
+ ## More Information
414
+
415
+ - [Ruff Documentation](https://docs.astral.sh/ruff/)
416
+ - [Python Packaging User Guide](https://packaging.python.org/)
417
+ - [Twine Documentation](https://twine.readthedocs.io/)
418
+ - [Semantic Versioning](https://semver.org/)
@@ -10,4 +10,5 @@ Understanding-oriented deep dives into the library's design and advanced feature
10
10
  advanced-features
11
11
  architecture
12
12
  tank-energy
13
+ tou-recovery-cap
13
14
  unknown-values