python-bsblan 6.1.6__tar.gz → 6.1.8__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 (179) hide show
  1. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/codeql.yaml +3 -3
  2. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/dependency-review.yaml +1 -1
  3. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/docs.yaml +2 -2
  4. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/labels.yaml +1 -1
  5. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/linting.yaml +5 -5
  6. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/release-drafter.yaml +1 -1
  7. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/release.yaml +3 -3
  8. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/scorecard.yml +3 -3
  9. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/stale.yaml +1 -1
  10. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/tests.yaml +4 -4
  11. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/typing.yaml +2 -2
  12. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/PKG-INFO +1 -1
  13. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/api/exceptions.md +8 -0
  14. python_bsblan-6.1.8/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/.openspec.yaml +3 -0
  15. python_bsblan-6.1.8/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/design.md +101 -0
  16. python_bsblan-6.1.8/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/proposal.md +49 -0
  17. python_bsblan-6.1.8/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/specs/error-classification/spec.md +50 -0
  18. python_bsblan-6.1.8/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/tasks.md +28 -0
  19. python_bsblan-6.1.8/openspec/changes/archive/2026-07-16-fix-partial-validation-state/.openspec.yaml +3 -0
  20. python_bsblan-6.1.8/openspec/changes/archive/2026-07-16-fix-partial-validation-state/design.md +86 -0
  21. python_bsblan-6.1.8/openspec/changes/archive/2026-07-16-fix-partial-validation-state/proposal.md +38 -0
  22. python_bsblan-6.1.8/openspec/changes/archive/2026-07-16-fix-partial-validation-state/specs/lazy-validation-completeness/spec.md +63 -0
  23. python_bsblan-6.1.8/openspec/changes/archive/2026-07-16-fix-partial-validation-state/tasks.md +29 -0
  24. python_bsblan-6.1.8/openspec/specs/error-classification/spec.md +59 -0
  25. python_bsblan-6.1.8/openspec/specs/lazy-validation-completeness/spec.md +71 -0
  26. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/package-lock.json +4 -4
  27. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/package.json +1 -1
  28. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/pyproject.toml +2 -2
  29. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/__init__.py +4 -0
  30. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_hot_water.py +7 -2
  31. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_schedules.py +4 -3
  32. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_transport.py +5 -3
  33. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_validation.py +104 -51
  34. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/exceptions.py +17 -0
  35. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/models.py +7 -2
  36. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/utility.py +31 -6
  37. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_api_validation.py +130 -0
  38. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_bsblan_edge_cases.py +11 -2
  39. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_entity_info.py +2 -1
  40. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_heating_schedule.py +9 -3
  41. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_hot_water_additional.py +193 -12
  42. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_include_parameter.py +3 -2
  43. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_reset_validation.py +12 -0
  44. python_bsblan-6.1.6/.github/hooks/run-validation-after-edits.json +0 -11
  45. python_bsblan-6.1.6/.github/hooks/run_validation_after_edits.sh +0 -92
  46. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.editorconfig +0 -0
  47. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.gitattributes +0 -0
  48. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/CODE_OF_CONDUCT.md +0 -0
  49. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/CONTRIBUTING.md +0 -0
  50. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  51. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  52. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  53. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/SECURITY.md +0 -0
  54. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/agents/pr-review.agent.md +0 -0
  55. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/agents/security-reviewer.agent.md +0 -0
  56. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/agents/tdd-refactor.agent.md +0 -0
  57. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/copilot-instructions.md +0 -0
  58. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/labels.yml +0 -0
  59. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/add-parameter.prompt.md +0 -0
  60. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/code-review.prompt.md +0 -0
  61. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/opsx-apply.prompt.md +0 -0
  62. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/opsx-archive.prompt.md +0 -0
  63. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/opsx-explore.prompt.md +0 -0
  64. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/opsx-propose.prompt.md +0 -0
  65. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/prompts/opsx-sync.prompt.md +0 -0
  66. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/release-drafter.yml +0 -0
  67. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/renovate.json +0 -0
  68. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/bsblan-parameters/SKILL.md +0 -0
  69. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/bsblan-testing/SKILL.md +0 -0
  70. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/feature-doc-updates/SKILL.md +0 -0
  71. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/openspec-apply-change/SKILL.md +0 -0
  72. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/openspec-archive-change/SKILL.md +0 -0
  73. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/openspec-explore/SKILL.md +0 -0
  74. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/openspec-propose/SKILL.md +0 -0
  75. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/skills/openspec-sync-specs/SKILL.md +0 -0
  76. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/auto-approve-renovate.yml +0 -0
  77. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/lock.yaml +0 -0
  78. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/workflows/pr-labels.yaml +0 -0
  79. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.github/zizmor.yml +0 -0
  80. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.gitignore +0 -0
  81. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.nvmrc +0 -0
  82. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.pre-commit-config.yaml +0 -0
  83. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.prettierignore +0 -0
  84. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/.yamllint +0 -0
  85. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/AGENTS.md +0 -0
  86. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/CLAUDE.md +0 -0
  87. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/LICENSE.md +0 -0
  88. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/Makefile +0 -0
  89. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/README.md +0 -0
  90. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/api/client.md +0 -0
  91. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/api/constants.md +0 -0
  92. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/api/models.md +0 -0
  93. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/getting-started.md +0 -0
  94. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/docs/index.md +0 -0
  95. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/control.py +0 -0
  96. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/discovery.py +0 -0
  97. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/fetch_param.py +0 -0
  98. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/profile_init.py +0 -0
  99. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/ruff.toml +0 -0
  100. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/examples/speed_test.py +0 -0
  101. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/mkdocs.yml +0 -0
  102. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/.openspec.yaml +0 -0
  103. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/design.md +0 -0
  104. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/proposal.md +0 -0
  105. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/specs/cooling-operating-mode/spec.md +0 -0
  106. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/tasks.md +0 -0
  107. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/.openspec.yaml +0 -0
  108. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/design.md +0 -0
  109. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/proposal.md +0 -0
  110. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/specs/circuit-discovery/spec.md +0 -0
  111. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/tasks.md +0 -0
  112. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/.openspec.yaml +0 -0
  113. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/design.md +0 -0
  114. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/proposal.md +0 -0
  115. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/specs/client-retry-policy/spec.md +0 -0
  116. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/tasks.md +0 -0
  117. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/config.yaml +0 -0
  118. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/specs/circuit-discovery/spec.md +0 -0
  119. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/specs/client-retry-policy/spec.md +0 -0
  120. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/openspec/specs/cooling-operating-mode/spec.md +0 -0
  121. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/sonar-project.properties +0 -0
  122. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_parameters.py +0 -0
  123. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_temperature.py +0 -0
  124. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_thermostat.py +0 -0
  125. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/_version.py +0 -0
  126. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/bsblan.py +0 -0
  127. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/constants.py +0 -0
  128. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/src/bsblan/py.typed +0 -0
  129. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/__init__.py +0 -0
  130. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/conftest.py +0 -0
  131. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/device.json +0 -0
  132. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/dict_version.json +0 -0
  133. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/hot_water_state.json +0 -0
  134. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/info.json +0 -0
  135. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/password.txt +0 -0
  136. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/pps_device.json +0 -0
  137. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/pps_state.json +0 -0
  138. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/pps_static_values.json +0 -0
  139. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/sensor.json +0 -0
  140. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/state.json +0 -0
  141. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/state_circuit2.json +0 -0
  142. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/static_state.json +0 -0
  143. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/static_state_circuit2.json +0 -0
  144. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/thermostat_hvac.json +0 -0
  145. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/thermostat_temp.json +0 -0
  146. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/fixtures/time.json +0 -0
  147. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/ruff.toml +0 -0
  148. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_api_initialization.py +0 -0
  149. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_auth.py +0 -0
  150. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_backoff_retry.py +0 -0
  151. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_bsblan.py +0 -0
  152. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_circuit.py +0 -0
  153. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_configuration.py +0 -0
  154. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_constants.py +0 -0
  155. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_context_manager.py +0 -0
  156. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_device.py +0 -0
  157. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_dhw_time_switch.py +0 -0
  158. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_entity_info_ha.py +0 -0
  159. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_hotwater_state.py +0 -0
  160. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_info.py +0 -0
  161. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_initialization.py +0 -0
  162. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_pps.py +0 -0
  163. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_read_parameters.py +0 -0
  164. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_response_encoding.py +0 -0
  165. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_schedule_models.py +0 -0
  166. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_sensor.py +0 -0
  167. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_set_heating_schedule.py +0 -0
  168. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_set_hot_water_schedule.py +0 -0
  169. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_set_hotwater.py +0 -0
  170. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_state.py +0 -0
  171. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_static_state.py +0 -0
  172. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_temperature_unit.py +0 -0
  173. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_temperature_validation.py +0 -0
  174. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_thermostat.py +0 -0
  175. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_time.py +0 -0
  176. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_utility.py +0 -0
  177. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_utility_additional.py +0 -0
  178. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_utility_edge_cases.py +0 -0
  179. {python_bsblan-6.1.6 → python_bsblan-6.1.8}/tests/test_version_errors.py +0 -0
@@ -23,10 +23,10 @@ jobs:
23
23
  security-events: write
24
24
  steps:
25
25
  - name: ⤵️ Check out code from GitHub
26
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
26
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
27
27
  with:
28
28
  persist-credentials: false
29
29
  - name: 🏗 Initialize CodeQL
30
- uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
30
+ uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
31
31
  - name: 🚀 Perform CodeQL Analysis
32
- uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
32
+ uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
@@ -13,7 +13,7 @@ jobs:
13
13
  steps:
14
14
  - name: ⤵️ Check out code from GitHub
15
15
  # yamllint disable-line rule:line-length
16
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
16
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
17
17
  with:
18
18
  persist-credentials: false
19
19
  - name: 👀 Dependency review
@@ -23,11 +23,11 @@ jobs:
23
23
  runs-on: ubuntu-latest
24
24
  steps:
25
25
  - name: ⤵️ Check out code from GitHub
26
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
26
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
27
27
  with:
28
28
  persist-credentials: false
29
29
  - name: 🏗 Set up uv
30
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
31
31
  with:
32
32
  enable-cache: true
33
33
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -21,7 +21,7 @@ jobs:
21
21
  issues: write
22
22
  steps:
23
23
  - name: ⤵️ Check out code from GitHub
24
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
24
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
25
25
  with:
26
26
  persist-credentials: false
27
27
  - name: 🚀 Run Label Syncer
@@ -21,11 +21,11 @@ jobs:
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
23
  - name: ⤵️ Check out code from GitHub
24
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
24
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
25
25
  with:
26
26
  persist-credentials: false
27
27
  - name: 🏗 Set up uv
28
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
29
29
  with:
30
30
  enable-cache: true
31
31
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -45,11 +45,11 @@ jobs:
45
45
  runs-on: ubuntu-latest
46
46
  steps:
47
47
  - name: ⤵️ Check out code from GitHub
48
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
48
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
49
49
  with:
50
50
  persist-credentials: false
51
51
  - name: 🏗 Set up uv
52
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
52
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
53
53
  with:
54
54
  enable-cache: true
55
55
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -60,7 +60,7 @@ jobs:
60
60
  - name: 🏗 Install Python dependencies
61
61
  run: uv sync --dev
62
62
  - name: 🏗 Set up Node.js
63
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
63
+ uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
64
64
  with:
65
65
  node-version-file: ".nvmrc"
66
66
  cache: "npm"
@@ -36,7 +36,7 @@ jobs:
36
36
  steps:
37
37
  - name: 🚀 Run Release Drafter
38
38
  # yamllint disable-line rule:line-length
39
- uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
39
+ uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
40
40
  with:
41
41
  prerelease: ${{ github.event.inputs.prerelease == 'true' }}
42
42
  prerelease-identifier: ${{ github.event.inputs.prerelease_identifier }}
@@ -25,11 +25,11 @@ jobs:
25
25
  id-token: write
26
26
  steps:
27
27
  - name: ⤵️ Check out code from GitHub
28
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
28
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
29
29
  with:
30
30
  persist-credentials: false
31
31
  - name: 🏗 Set up uv
32
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
32
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
33
33
  with:
34
34
  enable-cache: false
35
35
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -54,7 +54,7 @@ jobs:
54
54
  with:
55
55
  subject-path: ./dist/*
56
56
  - name: 🚀 Publish to PyPi
57
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
57
+ uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
58
58
  with:
59
59
  verbose: true
60
60
  print-hash: true
@@ -34,13 +34,13 @@ jobs:
34
34
  steps:
35
35
  - name: "Checkout code"
36
36
  # yamllint disable-line rule:line-length
37
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
37
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
38
38
  with:
39
39
  persist-credentials: false
40
40
 
41
41
  - name: "Run analysis"
42
42
  # yamllint disable-line rule:line-length
43
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
43
+ uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
44
44
  with:
45
45
  results_file: results.sarif
46
46
  results_format: sarif
@@ -66,6 +66,6 @@ jobs:
66
66
  # Upload the results to GitHub's code scanning dashboard (optional).
67
67
  - name: "Upload to code-scanning"
68
68
  # yamllint disable-line rule:line-length
69
- uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
69
+ uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
70
70
  with:
71
71
  sarif_file: results.sarif
@@ -18,7 +18,7 @@ jobs:
18
18
  pull-requests: write
19
19
  steps:
20
20
  - name: 🚀 Run stale
21
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
21
+ uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
22
22
  with:
23
23
  repo-token: ${{ secrets.GITHUB_TOKEN }}
24
24
  days-before-stale: 30
@@ -29,11 +29,11 @@ jobs:
29
29
  python: ["3.12", "3.13", "3.14"]
30
30
  steps:
31
31
  - name: ⤵️ Check out code from GitHub
32
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
32
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
33
33
  with:
34
34
  persist-credentials: false
35
35
  - name: 🏗 Set up uv
36
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
36
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
37
37
  with:
38
38
  enable-cache: true
39
39
  - name: 🏗 Set up Python ${{ matrix.python }}
@@ -61,14 +61,14 @@ jobs:
61
61
  pull-requests: read
62
62
  steps:
63
63
  - name: ⤵️ Check out code from GitHub
64
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
64
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
65
65
  with:
66
66
  fetch-depth: 0
67
67
  persist-credentials: false
68
68
  - name: ⬇️ Download coverage data
69
69
  uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
70
70
  - name: 🏗 Set up uv
71
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
71
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
72
72
  with:
73
73
  enable-cache: true
74
74
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -22,12 +22,12 @@ jobs:
22
22
  steps:
23
23
  - name: ⤵️ Check out code from GitHub
24
24
  # yamllint disable-line rule:line-length
25
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
25
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
26
26
  with:
27
27
  persist-credentials: false
28
28
  - name: 🏗 Set up uv
29
29
  # yamllint disable-line rule:line-length
30
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
31
31
  with:
32
32
  enable-cache: true
33
33
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-bsblan
3
- Version: 6.1.6
3
+ Version: 6.1.8
4
4
  Summary: Asynchronous Python client for BSBLAN API
5
5
  Project-URL: Homepage, https://github.com/liudger/python-bsblan
6
6
  Project-URL: Repository, https://github.com/liudger/python-bsblan
@@ -13,3 +13,11 @@ Custom exceptions raised by the library.
13
13
  ## BSBLANAuthError
14
14
 
15
15
  ::: bsblan.BSBLANAuthError
16
+
17
+ ## BSBLANUnsupportedFeatureError
18
+
19
+ ::: bsblan.BSBLANUnsupportedFeatureError
20
+
21
+ ## BSBLANMalformedResponseError
22
+
23
+ ::: bsblan.BSBLANMalformedResponseError
@@ -0,0 +1,3 @@
1
+ ---
2
+ schema: spec-driven
3
+ created: 2026-07-12
@@ -0,0 +1,101 @@
1
+ ## Context
2
+
3
+ `python-bsblan` currently exposes a single generic `BSBLANError` for a wide
4
+ range of failure modes. Two of them are semantically opposite from a retry
5
+ standpoint:
6
+
7
+ - **Unsupported schedule** — the device does not expose the requested
8
+ time-program parameters. In `_schedules.heating_schedule()` this raises
9
+ `BSBLANError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)`, and in
10
+ `_hot_water._get_filtered_data()` it raises `BSBLANError(error_msg)` for the
11
+ hot-water schedule group. This is a **permanent** condition.
12
+ - **Malformed response** — the transport cannot decode or JSON-parse the body.
13
+ In `_transport.request_with_retry()` this is caught from `ValueError` /
14
+ `UnicodeDecodeError` and re-raised as
15
+ `BSBLANError(ErrorMsg.INVALID_RESPONSE.format(e))`. This is a **transient**
16
+ condition.
17
+
18
+ Because both surface as `BSBLANError`, a downstream integration (Home Assistant)
19
+ cannot decide whether to retry. It must either stop retrying (losing recovery
20
+ from transient malformed responses) or retry forever (uselessly hammering a
21
+ device that will never support a schedule after a write).
22
+
23
+ The library's own `backoff` decorator only retries `TimeoutError` and
24
+ `aiohttp.ClientError`, so `BSBLANError` is never retried inside the library; the
25
+ retry decision lives in the consumer. The fix is therefore an additive exception
26
+ taxonomy, not a change to the transport retry policy.
27
+
28
+ ## Goals / Non-Goals
29
+
30
+ **Goals:**
31
+ - Let callers distinguish permanent unsupported-feature failures from transient
32
+ malformed-response failures by `except` type.
33
+ - Keep the change fully backward compatible with existing `except BSBLANError`
34
+ handlers.
35
+ - Reuse existing `ErrorMsg` strings; no message wording changes.
36
+
37
+ **Non-Goals:**
38
+ - Changing the transport-level `backoff` retry policy or which HTTP statuses are
39
+ retried (`client-retry-policy` is unchanged).
40
+ - Adding automatic retry logic inside the library for schedules.
41
+ - Reclassifying other existing `BSBLANError` raise sites beyond schedule reads
42
+ and malformed-response parsing.
43
+
44
+ ## Decisions
45
+
46
+ **Decision: Two new subclasses of `BSBLANError`.**
47
+ Add `BSBLANUnsupportedFeatureError` (permanent) and
48
+ `BSBLANMalformedResponseError` (transient), both subclassing `BSBLANError`.
49
+ Subclassing preserves backward compatibility: any existing `except BSBLANError`
50
+ keeps catching them.
51
+ - *Alternative considered:* a single new error with a `retryable: bool`
52
+ attribute. Rejected — attribute inspection is less idiomatic than `except`
53
+ type matching and easier to get wrong at call sites.
54
+ - *Alternative considered:* schedule-specific name (e.g.
55
+ `BSBLANUnsupportedScheduleError`). Chose the more general
56
+ `BSBLANUnsupportedFeatureError` because the same permanent semantics apply to
57
+ both heating and hot-water schedule reads and can extend to other
58
+ unsupported-parameter cases later.
59
+
60
+ **Decision: Raise the permanent error at the schedule-read guard sites.**
61
+ Replace the bare `BSBLANError(...)` at
62
+ `_schedules.heating_schedule()` (no mapped data) and the hot-water schedule
63
+ branch in `_hot_water._get_filtered_data()` with
64
+ `BSBLANUnsupportedFeatureError`, keeping the existing `ErrorMsg` text. The
65
+ hot-water helper is shared by state/config/schedule reads, so scope the new
66
+ error to the schedule group only (via `group_name == "schedule"`) to avoid
67
+ reclassifying unrelated state/config failures.
68
+
69
+ **Decision: Raise the transient error at the transport parse site.**
70
+ In `_transport.request_with_retry()`, replace the
71
+ `raise BSBLANError(msg)` in the `(ValueError, UnicodeDecodeError)` handler with
72
+ `raise BSBLANMalformedResponseError(msg)`, reusing
73
+ `ErrorMsg.INVALID_RESPONSE`.
74
+
75
+ **Decision: Export the new exceptions from the package.**
76
+ Add both classes to `bsblan.exceptions` and to the package `__all__` /
77
+ re-exports so consumers can import them from the top-level `bsblan` namespace.
78
+
79
+ ## Risks / Trade-offs
80
+
81
+ - **[Consumers catching the narrow generic message string]** → Mitigation:
82
+ messages are unchanged; only the concrete type is narrower. Broad
83
+ `except BSBLANError` still works.
84
+ - **[Over-scoping the hot-water helper]** → Mitigation: gate the new error on the
85
+ schedule group so state/config reads keep raising the existing generic error
86
+ and existing tests stay green.
87
+ - **[Missed raise site]** → Mitigation: grep for `NO_HEATING_SCHEDULE_PARAMS`,
88
+ the hot-water schedule `error_msg`, and `INVALID_RESPONSE` to confirm all
89
+ targeted sites are updated; add tests asserting the concrete types.
90
+
91
+ ## Migration Plan
92
+
93
+ Additive, no migration required. Release as a minor version. Consumers may
94
+ optionally add narrower `except BSBLANUnsupportedFeatureError` /
95
+ `except BSBLANMalformedResponseError` handlers; existing code is unaffected.
96
+
97
+ ## Open Questions
98
+
99
+ - Final class names: `BSBLANUnsupportedFeatureError` vs
100
+ `BSBLANUnsupportedScheduleError`. Proposed the broader name; confirm during
101
+ review.
@@ -0,0 +1,49 @@
1
+ ## Why
2
+
3
+ In python-bsblan 6.1.6 both an *unsupported schedule* (a device that will never
4
+ return the requested time-program parameters) and a *malformed response* (a
5
+ transient decode/parse failure) are surfaced as the same generic `BSBLANError`.
6
+ Downstream integrations such as Home Assistant cannot tell them apart by
7
+ exception type, so they must choose between stopping retries on a transient
8
+ malformed response or retrying an unsupported schedule indefinitely after a
9
+ write. A dedicated exception taxonomy is needed at the library level to make the
10
+ permanent-vs-transient distinction explicit.
11
+
12
+ ## What Changes
13
+
14
+ - Add a dedicated **permanent** exception `BSBLANUnsupportedFeatureError`
15
+ (subclass of `BSBLANError`) raised when a device does not expose the requested
16
+ schedule parameters, so callers know retrying will never succeed.
17
+ - Add a dedicated **transient** exception `BSBLANMalformedResponseError`
18
+ (subclass of `BSBLANError`) raised when a response body cannot be decoded or
19
+ parsed as valid JSON, signalling that a retry may succeed.
20
+ - Raise `BSBLANUnsupportedFeatureError` in the heating- and hot-water-schedule
21
+ read paths where a bare `BSBLANError` is currently raised for absent schedule
22
+ parameters.
23
+ - Raise `BSBLANMalformedResponseError` in the transport layer where invalid
24
+ JSON / decode errors are currently wrapped in a bare `BSBLANError`.
25
+ - Both new exceptions subclass `BSBLANError`, so existing `except BSBLANError`
26
+ handlers keep working (non-breaking).
27
+
28
+ ## Capabilities
29
+
30
+ ### New Capabilities
31
+ - `error-classification`: A library exception taxonomy that distinguishes
32
+ permanent unsupported-feature failures from transient malformed-response
33
+ failures, letting callers make correct retry decisions.
34
+
35
+ ### Modified Capabilities
36
+ <!-- No existing spec's requirements change; client-retry-policy behavior is unaffected. -->
37
+
38
+ ## Impact
39
+
40
+ - `src/bsblan/exceptions.py`: two new exception classes.
41
+ - `src/bsblan/_schedules.py` and `src/bsblan/_hot_water.py`: raise the new
42
+ unsupported-feature error for absent schedule parameters.
43
+ - `src/bsblan/_transport.py`: raise the new malformed-response error for
44
+ decode/parse failures.
45
+ - `src/bsblan/constants.py`: no new messages required; existing `ErrorMsg`
46
+ entries are reused.
47
+ - `tests/`: new coverage asserting the specific exception types are raised.
48
+ - Public API: additive only. Consumers relying on `BSBLANError` are unaffected;
49
+ consumers can opt into finer-grained handling.
@@ -0,0 +1,50 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Unsupported schedules raise a permanent error
4
+
5
+ The client SHALL raise `BSBLANUnsupportedFeatureError` (a subclass of
6
+ `BSBLANError`) when a schedule read cannot proceed because the device exposes no
7
+ schedule parameters for the requested feature. This error MUST signal a
8
+ permanent condition so callers know that retrying the same request will never
9
+ succeed.
10
+
11
+ #### Scenario: Heating schedule parameters absent
12
+
13
+ - **WHEN** `heating_schedule()` is called for a circuit whose time-program
14
+ parameters are not present in the device response (no schedule parameters can
15
+ be mapped)
16
+ - **THEN** the client raises `BSBLANUnsupportedFeatureError`
17
+ - **AND** the raised exception is also an instance of `BSBLANError`
18
+
19
+ #### Scenario: Hot water schedule parameters absent
20
+
21
+ - **WHEN** `schedule()` is called on a device that exposes no hot-water
22
+ time-program parameters after include filtering
23
+ - **THEN** the client raises `BSBLANUnsupportedFeatureError`
24
+ - **AND** the raised exception is also an instance of `BSBLANError`
25
+
26
+ ### Requirement: Malformed responses raise a transient error
27
+
28
+ The client SHALL raise `BSBLANMalformedResponseError` (a subclass of
29
+ `BSBLANError`) when a device response body cannot be decoded or parsed as valid
30
+ JSON. This error MUST signal a transient condition so callers know that a retry
31
+ may succeed.
32
+
33
+ #### Scenario: Response body is not valid JSON
34
+
35
+ - **WHEN** a request receives an HTTP `200` response whose body cannot be parsed
36
+ as JSON
37
+ - **THEN** the client raises `BSBLANMalformedResponseError`
38
+ - **AND** the raised exception is also an instance of `BSBLANError`
39
+
40
+ ### Requirement: New exceptions preserve backward compatibility
41
+
42
+ Both `BSBLANUnsupportedFeatureError` and `BSBLANMalformedResponseError` SHALL
43
+ subclass `BSBLANError` so that existing `except BSBLANError` handlers continue to
44
+ catch them without modification.
45
+
46
+ #### Scenario: Existing broad handler still catches new errors
47
+
48
+ - **WHEN** code catches `BSBLANError` around a call that raises either
49
+ `BSBLANUnsupportedFeatureError` or `BSBLANMalformedResponseError`
50
+ - **THEN** the exception is caught by the `BSBLANError` handler
@@ -0,0 +1,28 @@
1
+ ## 1. Exceptions
2
+
3
+ - [x] 1.1 Add `BSBLANUnsupportedFeatureError(BSBLANError)` to `src/bsblan/exceptions.py` with a docstring describing it as a permanent (non-retryable) condition.
4
+ - [x] 1.2 Add `BSBLANMalformedResponseError(BSBLANError)` to `src/bsblan/exceptions.py` with a docstring describing it as a transient (retryable) condition.
5
+ - [x] 1.3 Import and re-export both new exceptions in `src/bsblan/__init__.py` and add them to `__all__`.
6
+
7
+ ## 2. Schedule read paths (permanent error)
8
+
9
+ - [x] 2.1 In `src/bsblan/_schedules.py` `heating_schedule()`, raise `BSBLANUnsupportedFeatureError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)` when no schedule parameters are mapped (replace the bare `BSBLANError`).
10
+ - [x] 2.2 In `src/bsblan/_hot_water.py` `fetch_data()`, raise `BSBLANUnsupportedFeatureError(error_msg)` when the schedule group (`group_name == "schedule"`) has no filtered params, keeping other groups on the existing generic error.
11
+ - [x] 2.3 Update the `Raises:` docstrings on the affected schedule methods to reference the new exception type.
12
+
13
+ ## 3. Transport parse path (transient error)
14
+
15
+ - [x] 3.1 In `src/bsblan/_transport.py` `request_with_retry()`, raise `BSBLANMalformedResponseError(msg)` from the `(ValueError, UnicodeDecodeError)` handler (replace the bare `BSBLANError`), reusing `ErrorMsg.INVALID_RESPONSE`.
16
+ - [x] 3.2 Update the `Raises:` docstring on `request_with_retry()` to reference the new exception type.
17
+
18
+ ## 4. Tests
19
+
20
+ - [x] 4.1 Add a test asserting `heating_schedule()` raises `BSBLANUnsupportedFeatureError` when schedule params are absent, and that it is also a `BSBLANError`.
21
+ - [x] 4.2 Add a test asserting hot-water `schedule()` raises `BSBLANUnsupportedFeatureError` when schedule params are absent.
22
+ - [x] 4.3 Add a test asserting a non-JSON / undecodable response raises `BSBLANMalformedResponseError` and that it is also a `BSBLANError`.
23
+ - [x] 4.4 Verify existing state/config hot-water tests still raise the generic `BSBLANError` (no regression from the group gating).
24
+
25
+ ## 5. Documentation & validation
26
+
27
+ - [x] 5.1 Document the two new exceptions in `docs/api/exceptions.md`.
28
+ - [x] 5.2 Run `uv run prek run --all-files` and `uv run pytest --cov=src/bsblan --cov-report=term-missing`; ensure total coverage stays >= 95% and patch coverage is 100%.
@@ -0,0 +1,3 @@
1
+ ---
2
+ schema: spec-driven
3
+ created: 2026-07-16
@@ -0,0 +1,86 @@
1
+ ## Context
2
+
3
+ `SectionValidator` lazily validates device parameter support before a section or
4
+ hot-water group is read. It currently stores completion as a section-wide set in
5
+ `APIValidator` and a group-wide set in `SectionValidator`. An `include` request
6
+ validates only a subset but records that coarse completion state, so a later
7
+ request for different names or all names can skip validation.
8
+
9
+ The resulting configuration has not established whether every requested
10
+ parameter is supported. For normal sections, an unvalidated parameter can remain
11
+ in the configuration and later produce a strict response-mapping failure. For
12
+ hot water, the cache contains only the first subset and later reads silently use
13
+ that incomplete cache.
14
+
15
+ ## Goals / Non-Goals
16
+
17
+ **Goals:**
18
+
19
+ - Record lazy-validation coverage by parameter ID for sections and hot-water
20
+ groups.
21
+ - Revalidate only the parameter IDs required by a later request when they have
22
+ not already been covered.
23
+ - Retain the existing public method signatures, include-filter validation, and
24
+ per-section/group locking model.
25
+ - Add regression tests for sequential filtered and unfiltered access.
26
+
27
+ **Non-Goals:**
28
+
29
+ - Change the public `include` API or make it accept parameter IDs.
30
+ - Alter device-response parsing, model serialization, or unsupported-parameter
31
+ rules.
32
+ - Retry or cache failed validation requests.
33
+
34
+ ## Decisions
35
+
36
+ ### Track covered parameter IDs per validation owner
37
+
38
+ `APIValidator` will maintain a mapping from section name to the set of parameter
39
+ IDs whose support was checked. `SectionValidator` will maintain the equivalent
40
+ mapping for hot-water group names. Completion for a request is determined by
41
+ whether its requested IDs are a subset of that owner’s covered IDs.
42
+
43
+ Parameter IDs are chosen over parameter names because they are the identifiers
44
+ sent to BSB-LAN and remain unambiguous if a configuration ever aliases a name.
45
+ When a response establishes an ID is unsupported, it is removed from the active
46
+ configuration/cache but remains covered, preventing needless repeated probes.
47
+
48
+ Alternative considered: avoid persisting any completion after an `include`
49
+ request. This avoids the defect but discards useful knowledge and repeatedly
50
+ validates already checked parameters. Parameter-level coverage keeps lazy
51
+ loading efficient while remaining correct.
52
+
53
+ ### Derive requested IDs before the one-time gate
54
+
55
+ Each ensure method will first resolve the configured IDs for its section or
56
+ group, applying `include` when present. Its lock’s fast and post-lock checks will
57
+ compare that specific requested-ID set to coverage rather than use a coarse
58
+ section/group marker. A full request resolves all currently configured IDs and
59
+ therefore completes only after each is covered.
60
+
61
+ An include filter that matches no IDs will not add coverage or mark a group or
62
+ section complete. Existing downstream include validation remains responsible for
63
+ returning the appropriate invalid-include error.
64
+
65
+ Alternative considered: retain a boolean for full validation plus a separate
66
+ partial set. This duplicates state and makes configuration changes harder to
67
+ reason about; one coverage mapping expresses both cases.
68
+
69
+ ### Preserve locks and recheck coverage after waiting
70
+
71
+ The existing per-section and per-group locks remain in place. The completion
72
+ predicate will calculate coverage for the current requested IDs both before and
73
+ after lock acquisition. Concurrent calls requesting different subsets therefore
74
+ serialize safely, and the second call still validates its uncovered IDs.
75
+
76
+ ## Risks / Trade-offs
77
+
78
+ - [Configuration mutation during validation] → Derive the requested IDs from
79
+ the current configuration at each completion check; removed unsupported IDs
80
+ no longer need validation.
81
+ - [Stale internal tests inspect completion sets] → Update tests to assert
82
+ parameter coverage and externally observable request/cache behavior instead
83
+ of obsolete whole-group completion.
84
+ - [Additional first-read requests] → A later request for previously unrequested
85
+ parameters necessarily makes one validation request; already covered IDs are
86
+ not re-requested.
@@ -0,0 +1,38 @@
1
+ ## Why
2
+
3
+ Lazy validation currently records a section or hot-water group as fully
4
+ validated after a request filtered with `include`. This can leave unsupported
5
+ parameters in a section or leave the hot-water cache incomplete, causing later
6
+ unfiltered reads to skip necessary validation and return incomplete data or fail
7
+ while mapping a device response.
8
+
9
+ ## What Changes
10
+
11
+ - Track validation coverage at the parameter level for sections and hot-water
12
+ groups instead of treating any successful filtered request as complete.
13
+ - Ensure a later request validates parameter names that an earlier `include`
14
+ request did not cover.
15
+ - Keep validation completion correct when an include filter has no matching
16
+ parameters.
17
+ - Add focused regression coverage for filtered-then-unfiltered and
18
+ differently-filtered reads.
19
+
20
+ ## Capabilities
21
+
22
+ ### New Capabilities
23
+
24
+ - `lazy-validation-completeness`: Lazy section and hot-water validation covers
25
+ every parameter required by each request before that request uses the cached
26
+ configuration.
27
+
28
+ ### Modified Capabilities
29
+
30
+ - None.
31
+
32
+ ## Impact
33
+
34
+ - Affected code: `src/bsblan/_validation.py`, `src/bsblan/utility.py`, and the
35
+ lazy section and hot-water read paths that use their validation state.
36
+ - Affected tests: validation, include-filter, and hot-water regression tests.
37
+ - Public API: no signature changes; `include` calls may perform an additional
38
+ validation request when they need parameters not checked by a prior call.