python-bsblan 6.1.5__tar.gz → 6.1.7__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 (173) hide show
  1. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/PKG-INFO +1 -1
  2. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/api/exceptions.md +8 -0
  3. python_bsblan-6.1.7/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/.openspec.yaml +3 -0
  4. python_bsblan-6.1.7/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/design.md +101 -0
  5. python_bsblan-6.1.7/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/proposal.md +49 -0
  6. python_bsblan-6.1.7/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/specs/error-classification/spec.md +50 -0
  7. python_bsblan-6.1.7/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/tasks.md +28 -0
  8. python_bsblan-6.1.7/openspec/specs/error-classification/spec.md +59 -0
  9. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/package-lock.json +4 -4
  10. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/package.json +1 -1
  11. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/pyproject.toml +2 -2
  12. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/__init__.py +4 -0
  13. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_hot_water.py +7 -2
  14. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_schedules.py +4 -3
  15. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_transport.py +34 -4
  16. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/exceptions.py +17 -0
  17. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_bsblan_edge_cases.py +12 -3
  18. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_heating_schedule.py +9 -3
  19. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_hot_water_additional.py +10 -4
  20. python_bsblan-6.1.7/tests/test_response_encoding.py +62 -0
  21. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.editorconfig +0 -0
  22. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.gitattributes +0 -0
  23. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/CODE_OF_CONDUCT.md +0 -0
  24. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/CONTRIBUTING.md +0 -0
  25. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/SECURITY.md +0 -0
  29. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/agents/pr-review.agent.md +0 -0
  30. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/agents/security-reviewer.agent.md +0 -0
  31. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/agents/tdd-refactor.agent.md +0 -0
  32. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/copilot-instructions.md +0 -0
  33. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/hooks/run-validation-after-edits.json +0 -0
  34. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/hooks/run_validation_after_edits.sh +0 -0
  35. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/labels.yml +0 -0
  36. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/add-parameter.prompt.md +0 -0
  37. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/code-review.prompt.md +0 -0
  38. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/opsx-apply.prompt.md +0 -0
  39. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/opsx-archive.prompt.md +0 -0
  40. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/opsx-explore.prompt.md +0 -0
  41. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/opsx-propose.prompt.md +0 -0
  42. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/prompts/opsx-sync.prompt.md +0 -0
  43. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/release-drafter.yml +0 -0
  44. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/renovate.json +0 -0
  45. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/bsblan-parameters/SKILL.md +0 -0
  46. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/bsblan-testing/SKILL.md +0 -0
  47. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/feature-doc-updates/SKILL.md +0 -0
  48. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/openspec-apply-change/SKILL.md +0 -0
  49. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/openspec-archive-change/SKILL.md +0 -0
  50. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/openspec-explore/SKILL.md +0 -0
  51. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/openspec-propose/SKILL.md +0 -0
  52. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/skills/openspec-sync-specs/SKILL.md +0 -0
  53. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/auto-approve-renovate.yml +0 -0
  54. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/codeql.yaml +0 -0
  55. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/dependency-review.yaml +0 -0
  56. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/docs.yaml +0 -0
  57. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/labels.yaml +0 -0
  58. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/linting.yaml +0 -0
  59. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/lock.yaml +0 -0
  60. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/pr-labels.yaml +0 -0
  61. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/release-drafter.yaml +0 -0
  62. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/release.yaml +0 -0
  63. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/scorecard.yml +0 -0
  64. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/stale.yaml +0 -0
  65. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/tests.yaml +0 -0
  66. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/workflows/typing.yaml +0 -0
  67. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.github/zizmor.yml +0 -0
  68. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.gitignore +0 -0
  69. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.nvmrc +0 -0
  70. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.pre-commit-config.yaml +0 -0
  71. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.prettierignore +0 -0
  72. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/.yamllint +0 -0
  73. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/AGENTS.md +0 -0
  74. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/CLAUDE.md +0 -0
  75. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/LICENSE.md +0 -0
  76. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/Makefile +0 -0
  77. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/README.md +0 -0
  78. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/api/client.md +0 -0
  79. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/api/constants.md +0 -0
  80. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/api/models.md +0 -0
  81. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/getting-started.md +0 -0
  82. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/docs/index.md +0 -0
  83. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/control.py +0 -0
  84. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/discovery.py +0 -0
  85. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/fetch_param.py +0 -0
  86. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/profile_init.py +0 -0
  87. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/ruff.toml +0 -0
  88. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/examples/speed_test.py +0 -0
  89. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/mkdocs.yml +0 -0
  90. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/.openspec.yaml +0 -0
  91. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/design.md +0 -0
  92. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/proposal.md +0 -0
  93. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/specs/cooling-operating-mode/spec.md +0 -0
  94. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/tasks.md +0 -0
  95. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/.openspec.yaml +0 -0
  96. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/design.md +0 -0
  97. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/proposal.md +0 -0
  98. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/specs/circuit-discovery/spec.md +0 -0
  99. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/tasks.md +0 -0
  100. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/.openspec.yaml +0 -0
  101. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/design.md +0 -0
  102. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/proposal.md +0 -0
  103. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/specs/client-retry-policy/spec.md +0 -0
  104. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/tasks.md +0 -0
  105. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/config.yaml +0 -0
  106. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/specs/circuit-discovery/spec.md +0 -0
  107. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/specs/client-retry-policy/spec.md +0 -0
  108. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/openspec/specs/cooling-operating-mode/spec.md +0 -0
  109. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/sonar-project.properties +0 -0
  110. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_parameters.py +0 -0
  111. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_temperature.py +0 -0
  112. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_thermostat.py +0 -0
  113. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_validation.py +0 -0
  114. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/_version.py +0 -0
  115. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/bsblan.py +0 -0
  116. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/constants.py +0 -0
  117. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/models.py +0 -0
  118. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/py.typed +0 -0
  119. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/src/bsblan/utility.py +0 -0
  120. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/__init__.py +0 -0
  121. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/conftest.py +0 -0
  122. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/device.json +0 -0
  123. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/dict_version.json +0 -0
  124. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/hot_water_state.json +0 -0
  125. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/info.json +0 -0
  126. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/password.txt +0 -0
  127. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/pps_device.json +0 -0
  128. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/pps_state.json +0 -0
  129. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/pps_static_values.json +0 -0
  130. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/sensor.json +0 -0
  131. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/state.json +0 -0
  132. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/state_circuit2.json +0 -0
  133. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/static_state.json +0 -0
  134. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/static_state_circuit2.json +0 -0
  135. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/thermostat_hvac.json +0 -0
  136. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/thermostat_temp.json +0 -0
  137. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/fixtures/time.json +0 -0
  138. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/ruff.toml +0 -0
  139. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_api_initialization.py +0 -0
  140. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_api_validation.py +0 -0
  141. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_auth.py +0 -0
  142. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_backoff_retry.py +0 -0
  143. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_bsblan.py +0 -0
  144. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_circuit.py +0 -0
  145. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_configuration.py +0 -0
  146. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_constants.py +0 -0
  147. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_context_manager.py +0 -0
  148. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_device.py +0 -0
  149. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_dhw_time_switch.py +0 -0
  150. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_entity_info.py +0 -0
  151. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_entity_info_ha.py +0 -0
  152. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_hotwater_state.py +0 -0
  153. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_include_parameter.py +0 -0
  154. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_info.py +0 -0
  155. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_initialization.py +0 -0
  156. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_pps.py +0 -0
  157. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_read_parameters.py +0 -0
  158. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_reset_validation.py +0 -0
  159. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_schedule_models.py +0 -0
  160. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_sensor.py +0 -0
  161. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_set_heating_schedule.py +0 -0
  162. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_set_hot_water_schedule.py +0 -0
  163. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_set_hotwater.py +0 -0
  164. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_state.py +0 -0
  165. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_static_state.py +0 -0
  166. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_temperature_unit.py +0 -0
  167. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_temperature_validation.py +0 -0
  168. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_thermostat.py +0 -0
  169. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_time.py +0 -0
  170. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_utility.py +0 -0
  171. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_utility_additional.py +0 -0
  172. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_utility_edge_cases.py +0 -0
  173. {python_bsblan-6.1.5 → python_bsblan-6.1.7}/tests/test_version_errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-bsblan
3
- Version: 6.1.5
3
+ Version: 6.1.7
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,59 @@
1
+ # error-classification
2
+
3
+ ## Purpose
4
+
5
+ A library exception taxonomy that distinguishes permanent unsupported-feature
6
+ failures from transient malformed-response failures, letting callers make
7
+ correct retry decisions. Both new exceptions subclass `BSBLANError` so existing
8
+ broad handlers keep working.
9
+
10
+ ## Requirements
11
+
12
+ ### Requirement: Unsupported schedules raise a permanent error
13
+
14
+ The client SHALL raise `BSBLANUnsupportedFeatureError` (a subclass of
15
+ `BSBLANError`) when a schedule read cannot proceed because the device exposes no
16
+ schedule parameters for the requested feature. This error MUST signal a
17
+ permanent condition so callers know that retrying the same request will never
18
+ succeed.
19
+
20
+ #### Scenario: Heating schedule parameters absent
21
+
22
+ - **WHEN** `heating_schedule()` is called for a circuit whose time-program
23
+ parameters are not present in the device response (no schedule parameters can
24
+ be mapped)
25
+ - **THEN** the client raises `BSBLANUnsupportedFeatureError`
26
+ - **AND** the raised exception is also an instance of `BSBLANError`
27
+
28
+ #### Scenario: Hot water schedule parameters absent
29
+
30
+ - **WHEN** `schedule()` is called on a device that exposes no hot-water
31
+ time-program parameters after include filtering
32
+ - **THEN** the client raises `BSBLANUnsupportedFeatureError`
33
+ - **AND** the raised exception is also an instance of `BSBLANError`
34
+
35
+ ### Requirement: Malformed responses raise a transient error
36
+
37
+ The client SHALL raise `BSBLANMalformedResponseError` (a subclass of
38
+ `BSBLANError`) when a device response body cannot be decoded or parsed as valid
39
+ JSON. This error MUST signal a transient condition so callers know that a retry
40
+ may succeed.
41
+
42
+ #### Scenario: Response body is not valid JSON
43
+
44
+ - **WHEN** a request receives an HTTP `200` response whose body cannot be parsed
45
+ as JSON
46
+ - **THEN** the client raises `BSBLANMalformedResponseError`
47
+ - **AND** the raised exception is also an instance of `BSBLANError`
48
+
49
+ ### Requirement: New exceptions preserve backward compatibility
50
+
51
+ Both `BSBLANUnsupportedFeatureError` and `BSBLANMalformedResponseError` SHALL
52
+ subclass `BSBLANError` so that existing `except BSBLANError` handlers continue to
53
+ catch them without modification.
54
+
55
+ #### Scenario: Existing broad handler still catches new errors
56
+
57
+ - **WHEN** code catches `BSBLANError` around a call that raises either
58
+ `BSBLANUnsupportedFeatureError` or `BSBLANMalformedResponseError`
59
+ - **THEN** the exception is caught by the `BSBLANError` handler
@@ -12,13 +12,13 @@
12
12
  "url-parse": "1.5.10"
13
13
  },
14
14
  "devDependencies": {
15
- "prettier": "3.9.4"
15
+ "prettier": "3.9.5"
16
16
  }
17
17
  },
18
18
  "node_modules/prettier": {
19
- "version": "3.9.4",
20
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
21
- "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
19
+ "version": "3.9.5",
20
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz",
21
+ "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
22
22
  "dev": true,
23
23
  "license": "MIT",
24
24
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "author": "WJL van Rootselaar <liudgervr@gmail.com>",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "prettier": "3.9.4"
12
+ "prettier": "3.9.5"
13
13
  },
14
14
  "dependencies": {
15
15
  "url-parse": "1.5.10"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-bsblan"
3
- version = "6.1.5"
3
+ version = "6.1.7"
4
4
  description = "Asynchronous Python client for BSBLAN API"
5
5
  authors = [
6
6
  {name = "Willem-Jan van Rootselaar", email = "liudgervr@gmail.com"}
@@ -196,7 +196,7 @@ dev = [
196
196
  # hatch is required to support type hinting and proper packaging of the py.typed file.
197
197
  "hatch>=1.14.1",
198
198
  "isort==8.0.1",
199
- "ty==0.0.55",
199
+ "ty==0.0.56",
200
200
  "prek>=0.3.3",
201
201
  "pre-commit-hooks==6.0.0",
202
202
  "pylint==4.0.5",
@@ -12,6 +12,8 @@ from .exceptions import (
12
12
  BSBLANAuthError,
13
13
  BSBLANConnectionError,
14
14
  BSBLANError,
15
+ BSBLANMalformedResponseError,
16
+ BSBLANUnsupportedFeatureError,
15
17
  BSBLANVersionError,
16
18
  )
17
19
  from .models import (
@@ -45,6 +47,8 @@ __all__ = [
45
47
  "BSBLANConfig",
46
48
  "BSBLANConnectionError",
47
49
  "BSBLANError",
50
+ "BSBLANMalformedResponseError",
51
+ "BSBLANUnsupportedFeatureError",
48
52
  "BSBLANVersionError",
49
53
  "DHWSchedule",
50
54
  "DHWTimeSwitchPrograms",
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any, TypeVar
6
6
 
7
7
  from .constants import ErrorMsg, HotWaterParams
8
- from .exceptions import BSBLANError
8
+ from .exceptions import BSBLANError, BSBLANUnsupportedFeatureError
9
9
  from .models import (
10
10
  HotWaterConfig,
11
11
  HotWaterSchedule,
@@ -87,7 +87,10 @@ class HotWaterManager:
87
87
  The populated model instance.
88
88
 
89
89
  Raises:
90
- BSBLANError: If no parameters are available for the filter.
90
+ BSBLANUnsupportedFeatureError: If the schedule group exposes no
91
+ parameters (a permanent condition).
92
+ BSBLANError: If no parameters are available for a non-schedule
93
+ group.
91
94
 
92
95
  """
93
96
  # Granular lazy load: validate only this param group on first access
@@ -105,6 +108,8 @@ class HotWaterManager:
105
108
  filtered_params = self._apply_include_filter(filtered_params, include)
106
109
 
107
110
  if not filtered_params:
111
+ if group_name == "schedule":
112
+ raise BSBLANUnsupportedFeatureError(error_msg)
108
113
  raise BSBLANError(error_msg)
109
114
 
110
115
  data = await self._request_named_params(filtered_params)
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from .constants import ErrorMsg, HeatingScheduleParams, HotWaterParams
8
- from .exceptions import BSBLANError
8
+ from .exceptions import BSBLANError, BSBLANUnsupportedFeatureError
9
9
  from .models import HeatingTimeSwitchPrograms
10
10
 
11
11
  if TYPE_CHECKING:
@@ -62,7 +62,8 @@ class ScheduleManager:
62
62
  HeatingTimeSwitchPrograms: Heating schedule information.
63
63
 
64
64
  Raises:
65
- BSBLANError: If no schedule parameters are available.
65
+ BSBLANUnsupportedFeatureError: If the device exposes no heating
66
+ schedule parameters (a permanent condition).
66
67
 
67
68
  """
68
69
  self._validate_circuit(circuit)
@@ -79,7 +80,7 @@ class ScheduleManager:
79
80
  }
80
81
 
81
82
  if not mapped_data:
82
- raise BSBLANError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
83
+ raise BSBLANUnsupportedFeatureError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
83
84
 
84
85
  return HeatingTimeSwitchPrograms.model_validate(mapped_data)
85
86
 
@@ -9,6 +9,7 @@ stable ``_request`` facade that delegates here.
9
9
  from __future__ import annotations
10
10
 
11
11
  import asyncio
12
+ import json
12
13
  import logging
13
14
  from typing import TYPE_CHECKING, Any, cast
14
15
 
@@ -19,7 +20,7 @@ from packaging import version as pkg_version
19
20
  from yarl import URL
20
21
 
21
22
  from .constants import ErrorMsg
22
- from .exceptions import BSBLANAuthError, BSBLANError
23
+ from .exceptions import BSBLANAuthError, BSBLANError, BSBLANMalformedResponseError
23
24
 
24
25
  if TYPE_CHECKING:
25
26
  from collections.abc import Callable, Mapping
@@ -109,8 +110,10 @@ class BSBLANTransport:
109
110
  dict[str, Any]: The JSON response from the BSBLAN device.
110
111
 
111
112
  Raises:
112
- BSBLANError: If the session is missing or the response is invalid.
113
+ BSBLANError: If the session is missing.
113
114
  BSBLANAuthError: If authentication fails (401/403, not retried).
115
+ BSBLANMalformedResponseError: If the response body cannot be
116
+ decoded or parsed as JSON (a transient condition).
114
117
 
115
118
  """
116
119
  session = self._session_getter()
@@ -131,7 +134,7 @@ class BSBLANTransport:
131
134
  headers=headers,
132
135
  ) as response:
133
136
  response.raise_for_status()
134
- response_data = cast("dict[str, Any]", await response.json())
137
+ response_data = await self._read_json(response)
135
138
  return self._process_response(response_data, base_path)
136
139
  except aiohttp.ClientResponseError as e:
137
140
  if e.status in HTTP_AUTH_STATUSES:
@@ -140,7 +143,34 @@ class BSBLANTransport:
140
143
  except (ValueError, UnicodeDecodeError) as e:
141
144
  # Handle JSON decode errors and other parsing issues
142
145
  msg = ErrorMsg.INVALID_RESPONSE.format(e)
143
- raise BSBLANError(msg) from e
146
+ raise BSBLANMalformedResponseError(msg) from e
147
+
148
+ @staticmethod
149
+ async def _read_json(response: aiohttp.ClientResponse) -> dict[str, Any]:
150
+ """Decode a BSB-LAN JSON response tolerant of non-UTF-8 encodings.
151
+
152
+ Some BSB-LAN firmwares serve custom parameter descriptions using
153
+ Latin-1 (ISO-8859-1) bytes (for example the ``§`` or ``°`` characters)
154
+ while declaring no charset. ``aiohttp`` assumes UTF-8 and raises
155
+ ``UnicodeDecodeError``. Read the raw body and fall back to Latin-1,
156
+ which can decode any byte sequence, before parsing the JSON.
157
+
158
+ Args:
159
+ response: The active aiohttp response to read.
160
+
161
+ Returns:
162
+ dict[str, Any]: The parsed JSON payload.
163
+
164
+ Raises:
165
+ ValueError: If the body is not valid JSON.
166
+
167
+ """
168
+ raw = await response.read()
169
+ try:
170
+ text = raw.decode("utf-8")
171
+ except UnicodeDecodeError:
172
+ text = raw.decode("latin-1")
173
+ return cast("dict[str, Any]", json.loads(text))
144
174
 
145
175
  def _process_response(
146
176
  self, response_data: dict[str, Any], base_path: str
@@ -77,3 +77,20 @@ class BSBLANAuthError(BSBLANError):
77
77
  """Raised when authentication fails."""
78
78
 
79
79
  message: str = "Authentication failed. Please check your username and password."
80
+
81
+
82
+ class BSBLANUnsupportedFeatureError(BSBLANError):
83
+ """Raised when the device does not support a requested feature.
84
+
85
+ This signals a permanent condition: the device does not expose the
86
+ requested parameters (for example a heating or hot water schedule), so
87
+ retrying the same request will never succeed.
88
+ """
89
+
90
+
91
+ class BSBLANMalformedResponseError(BSBLANError):
92
+ """Raised when a device response cannot be decoded or parsed.
93
+
94
+ This signals a transient condition: the response body could not be decoded
95
+ or parsed as valid JSON, so a retry may succeed.
96
+ """
@@ -9,7 +9,11 @@ import aiohttp
9
9
  import pytest
10
10
 
11
11
  from bsblan import BSBLAN, BSBLANConfig
12
- from bsblan.exceptions import BSBLANConnectionError, BSBLANError
12
+ from bsblan.exceptions import (
13
+ BSBLANConnectionError,
14
+ BSBLANError,
15
+ BSBLANMalformedResponseError,
16
+ )
13
17
 
14
18
 
15
19
  @pytest.mark.asyncio
@@ -71,15 +75,20 @@ async def test_value_error_path(monkeypatch: Any) -> None:
71
75
  # Mock a successful response but with invalid JSON processing
72
76
  mock_response = MagicMock()
73
77
  mock_response.raise_for_status = MagicMock()
74
- mock_response.json = AsyncMock(side_effect=ValueError("Invalid JSON"))
78
+ mock_response.read = AsyncMock(return_value=b"Invalid JSON")
75
79
  mock_response.__aenter__ = AsyncMock(return_value=mock_response)
76
80
  mock_response.__aexit__ = AsyncMock(return_value=None)
77
81
 
78
82
  monkeypatch.setattr(session, "request", MagicMock(return_value=mock_response))
79
83
 
80
- with pytest.raises(BSBLANError, match="Invalid JSON"):
84
+ with pytest.raises(
85
+ BSBLANMalformedResponseError, match="Invalid response format"
86
+ ) as exc_info:
81
87
  await bsblan._request()
82
88
 
89
+ # Transient error must remain catchable as the generic BSBLANError.
90
+ assert isinstance(exc_info.value, BSBLANError)
91
+
83
92
 
84
93
  def test_bsblan_config_initialization_edge_cases() -> None:
85
94
  """Test edge cases in BSBLAN initialization."""
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock
7
7
 
8
8
  import pytest
9
9
 
10
- from bsblan.exceptions import BSBLANError
10
+ from bsblan.exceptions import BSBLANError, BSBLANUnsupportedFeatureError
11
11
  from bsblan.models import HeatingTimeSwitchPrograms
12
12
 
13
13
  if TYPE_CHECKING:
@@ -138,8 +138,14 @@ async def test_heating_schedule_invalid_include_raises(mock_bsblan: BSBLAN) -> N
138
138
 
139
139
  @pytest.mark.asyncio
140
140
  async def test_heating_schedule_no_params_raises(mock_bsblan: BSBLAN) -> None:
141
- """Test no schedule params in response raises error."""
141
+ """Test no schedule params in response raises unsupported-feature error."""
142
142
  mock_bsblan._request = AsyncMock(return_value={}) # type: ignore[method-assign]
143
143
 
144
- with pytest.raises(BSBLANError, match="No heating schedule parameters available"):
144
+ with pytest.raises(
145
+ BSBLANUnsupportedFeatureError,
146
+ match="No heating schedule parameters available",
147
+ ) as exc_info:
145
148
  await mock_bsblan.heating_schedule(circuit=1)
149
+
150
+ # Permanent error must remain catchable as the generic BSBLANError.
151
+ assert isinstance(exc_info.value, BSBLANError)
@@ -12,7 +12,7 @@ import pytest
12
12
 
13
13
  from bsblan import BSBLAN, BSBLANConfig
14
14
  from bsblan.constants import API_FULL
15
- from bsblan.exceptions import BSBLANError
15
+ from bsblan.exceptions import BSBLANError, BSBLANUnsupportedFeatureError
16
16
  from bsblan.models import HotWaterConfig, HotWaterSchedule
17
17
  from bsblan.utility import APIValidator
18
18
  from tests import load_fixture
@@ -253,11 +253,14 @@ async def test_hot_water_schedule_no_params_error(
253
253
  bsblan._validator._validated_hot_water_groups.add("schedule")
254
254
 
255
255
  with pytest.raises(
256
- BSBLANError,
256
+ BSBLANUnsupportedFeatureError,
257
257
  match="No hot water schedule parameters available",
258
- ):
258
+ ) as exc_info:
259
259
  await bsblan.hot_water_schedule()
260
260
 
261
+ # Permanent error must remain catchable as the generic BSBLANError.
262
+ assert isinstance(exc_info.value, BSBLANError)
263
+
261
264
 
262
265
  @pytest.mark.asyncio
263
266
  async def test_hot_water_state_no_params_error(
@@ -285,9 +288,12 @@ async def test_hot_water_state_no_params_error(
285
288
  with pytest.raises(
286
289
  BSBLANError,
287
290
  match="No essential hot water parameters available",
288
- ):
291
+ ) as exc_info:
289
292
  await bsblan.hot_water_state()
290
293
 
294
+ # State group must NOT be reclassified as an unsupported-feature error.
295
+ assert not isinstance(exc_info.value, BSBLANUnsupportedFeatureError)
296
+
291
297
 
292
298
  @pytest.mark.asyncio
293
299
  async def test_granular_hot_water_validation(