python-bsblan 6.1.11__tar.gz → 6.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/codeql.yaml +2 -2
  2. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/release.yaml +1 -1
  3. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/scorecard.yml +2 -2
  4. python_bsblan-6.2.0/.nvmrc +1 -0
  5. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/PKG-INFO +1 -1
  6. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/control.py +3 -5
  7. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/pyproject.toml +2 -2
  8. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/bsblan.py +15 -22
  9. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/constants.py +1 -0
  10. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/utility.py +0 -60
  11. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_pps.py +3 -2
  12. python_bsblan-6.2.0/tests/test_time.py +112 -0
  13. python_bsblan-6.2.0/tests/test_utility.py +173 -0
  14. python_bsblan-6.1.11/.nvmrc +0 -1
  15. python_bsblan-6.1.11/tests/test_time.py +0 -182
  16. python_bsblan-6.1.11/tests/test_utility.py +0 -398
  17. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.editorconfig +0 -0
  18. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.gitattributes +0 -0
  19. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/CODE_OF_CONDUCT.md +0 -0
  20. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/CONTRIBUTING.md +0 -0
  21. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/SECURITY.md +0 -0
  25. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/agents/pr-review.agent.md +0 -0
  26. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/agents/security-reviewer.agent.md +0 -0
  27. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/agents/tdd-refactor.agent.md +0 -0
  28. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/copilot-instructions.md +0 -0
  29. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/labels.yml +0 -0
  30. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/add-parameter.prompt.md +0 -0
  31. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/code-review.prompt.md +0 -0
  32. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/opsx-apply.prompt.md +0 -0
  33. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/opsx-archive.prompt.md +0 -0
  34. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/opsx-explore.prompt.md +0 -0
  35. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/opsx-propose.prompt.md +0 -0
  36. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/prompts/opsx-sync.prompt.md +0 -0
  37. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/release-drafter.yml +0 -0
  38. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/renovate.json +0 -0
  39. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/bsblan-parameters/SKILL.md +0 -0
  40. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/bsblan-testing/SKILL.md +0 -0
  41. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/feature-doc-updates/SKILL.md +0 -0
  42. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/openspec-apply-change/SKILL.md +0 -0
  43. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/openspec-archive-change/SKILL.md +0 -0
  44. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/openspec-explore/SKILL.md +0 -0
  45. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/openspec-propose/SKILL.md +0 -0
  46. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/skills/openspec-sync-specs/SKILL.md +0 -0
  47. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/auto-approve-renovate.yml +0 -0
  48. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/dependency-review.yaml +0 -0
  49. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/docs.yaml +0 -0
  50. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/labels.yaml +0 -0
  51. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/linting.yaml +0 -0
  52. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/lock.yaml +0 -0
  53. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/pr-labels.yaml +0 -0
  54. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/release-drafter.yaml +0 -0
  55. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/stale.yaml +0 -0
  56. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/tests.yaml +0 -0
  57. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/workflows/typing.yaml +0 -0
  58. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.github/zizmor.yml +0 -0
  59. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.gitignore +0 -0
  60. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.pre-commit-config.yaml +0 -0
  61. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.prettierignore +0 -0
  62. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/.yamllint +0 -0
  63. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/AGENTS.md +0 -0
  64. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/CLAUDE.md +0 -0
  65. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/LICENSE.md +0 -0
  66. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/Makefile +0 -0
  67. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/README.md +0 -0
  68. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/api/client.md +0 -0
  69. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/api/constants.md +0 -0
  70. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/api/exceptions.md +0 -0
  71. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/api/models.md +0 -0
  72. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/getting-started.md +0 -0
  73. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/docs/index.md +0 -0
  74. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/discovery.py +0 -0
  75. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/fake_bsblan_server.py +0 -0
  76. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/fetch_param.py +0 -0
  77. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/profile_init.py +0 -0
  78. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/ruff.toml +0 -0
  79. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/examples/speed_test.py +0 -0
  80. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/mkdocs.yml +0 -0
  81. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/.openspec.yaml +0 -0
  82. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/design.md +0 -0
  83. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/proposal.md +0 -0
  84. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/specs/cooling-operating-mode/spec.md +0 -0
  85. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-03-add-cooling-operating-mode/tasks.md +0 -0
  86. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/.openspec.yaml +0 -0
  87. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/design.md +0 -0
  88. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/proposal.md +0 -0
  89. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/specs/circuit-discovery/spec.md +0 -0
  90. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-harden-circuit-discovery-consistency/tasks.md +0 -0
  91. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/.openspec.yaml +0 -0
  92. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/design.md +0 -0
  93. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/proposal.md +0 -0
  94. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/specs/client-retry-policy/spec.md +0 -0
  95. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-04-improve-client-readability-domain-slices/tasks.md +0 -0
  96. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/.openspec.yaml +0 -0
  97. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/design.md +0 -0
  98. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/proposal.md +0 -0
  99. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/specs/error-classification/spec.md +0 -0
  100. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-12-distinguish-schedule-error-types/tasks.md +0 -0
  101. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/.openspec.yaml +0 -0
  102. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/design.md +0 -0
  103. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/proposal.md +0 -0
  104. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/specs/lazy-validation-completeness/spec.md +0 -0
  105. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/changes/archive/2026-07-16-fix-partial-validation-state/tasks.md +0 -0
  106. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/config.yaml +0 -0
  107. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/specs/circuit-discovery/spec.md +0 -0
  108. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/specs/client-retry-policy/spec.md +0 -0
  109. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/specs/cooling-operating-mode/spec.md +0 -0
  110. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/specs/error-classification/spec.md +0 -0
  111. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/openspec/specs/lazy-validation-completeness/spec.md +0 -0
  112. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/package-lock.json +0 -0
  113. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/package.json +0 -0
  114. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/sonar-project.properties +0 -0
  115. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/__init__.py +0 -0
  116. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_hot_water.py +0 -0
  117. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_parameters.py +0 -0
  118. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_schedules.py +0 -0
  119. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_temperature.py +0 -0
  120. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_thermostat.py +0 -0
  121. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_transport.py +0 -0
  122. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_validation.py +0 -0
  123. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/_version.py +0 -0
  124. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/exceptions.py +0 -0
  125. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/models.py +0 -0
  126. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/src/bsblan/py.typed +0 -0
  127. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/__init__.py +0 -0
  128. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/conftest.py +0 -0
  129. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/device.json +0 -0
  130. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/dict_version.json +0 -0
  131. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/hot_water_state.json +0 -0
  132. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/info.json +0 -0
  133. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/password.txt +0 -0
  134. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/pps_device.json +0 -0
  135. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/pps_state.json +0 -0
  136. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/pps_static_values.json +0 -0
  137. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/sensor.json +0 -0
  138. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/state.json +0 -0
  139. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/state_circuit2.json +0 -0
  140. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/static_state.json +0 -0
  141. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/static_state_circuit2.json +0 -0
  142. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/thermostat_hvac.json +0 -0
  143. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/thermostat_temp.json +0 -0
  144. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/fixtures/time.json +0 -0
  145. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/ruff.toml +0 -0
  146. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_api_initialization.py +0 -0
  147. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_api_validation.py +0 -0
  148. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_auth.py +0 -0
  149. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_backoff_retry.py +0 -0
  150. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_bsblan.py +0 -0
  151. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_bsblan_edge_cases.py +0 -0
  152. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_circuit.py +0 -0
  153. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_configuration.py +0 -0
  154. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_constants.py +0 -0
  155. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_context_manager.py +0 -0
  156. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_device.py +0 -0
  157. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_dhw_time_switch.py +0 -0
  158. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_entity_info.py +0 -0
  159. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_entity_info_ha.py +0 -0
  160. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_heating_schedule.py +0 -0
  161. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_hot_water_additional.py +0 -0
  162. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_hotwater_state.py +0 -0
  163. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_include_parameter.py +0 -0
  164. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_info.py +0 -0
  165. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_initialization.py +0 -0
  166. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_read_parameters.py +0 -0
  167. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_reset_validation.py +0 -0
  168. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_response_encoding.py +0 -0
  169. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_schedule_models.py +0 -0
  170. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_sensor.py +0 -0
  171. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_set_heating_schedule.py +0 -0
  172. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_set_hot_water_schedule.py +0 -0
  173. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_set_hotwater.py +0 -0
  174. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_state.py +0 -0
  175. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_static_state.py +0 -0
  176. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_temperature_unit.py +0 -0
  177. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_temperature_validation.py +0 -0
  178. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_thermostat.py +0 -0
  179. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_utility_additional.py +0 -0
  180. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_utility_edge_cases.py +0 -0
  181. {python_bsblan-6.1.11 → python_bsblan-6.2.0}/tests/test_version_errors.py +0 -0
@@ -27,6 +27,6 @@ jobs:
27
27
  with:
28
28
  persist-credentials: false
29
29
  - name: 🏗 Initialize CodeQL
30
- uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
30
+ uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
31
31
  - name: 🚀 Perform CodeQL Analysis
32
- uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
32
+ uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
@@ -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@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
57
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
58
58
  with:
59
59
  verbose: true
60
60
  print-hash: true
@@ -40,7 +40,7 @@ jobs:
40
40
 
41
41
  - name: "Run analysis"
42
42
  # yamllint disable-line rule:line-length
43
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
43
+ uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
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@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
69
+ uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
70
70
  with:
71
71
  sarif_file: results.sarif
@@ -0,0 +1 @@
1
+ 24.22.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-bsblan
3
- Version: 6.1.11
3
+ Version: 6.2.0
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
@@ -409,11 +409,9 @@ async def main() -> None:
409
409
  # Example: Set device time
410
410
  if bsblan.supports_time_sync:
411
411
  print("\nSetting device time to current system time")
412
- # Get current local system time and format it for BSB-LAN.
413
- current_time = datetime.now().replace(microsecond=0) # noqa: DTZ005
414
- formatted_time = current_time.strftime("%d.%m.%Y %H:%M:%S")
415
- print(f"Current system time: {formatted_time}")
416
- await bsblan.set_time(formatted_time)
412
+ current_time = datetime.now() # noqa: DTZ005
413
+ print(f"Current system time: {current_time:%d.%m.%Y %H:%M:%S}")
414
+ await bsblan.set_time(current_time)
417
415
  else:
418
416
  print("\nSkipping device time sync for this bus type")
419
417
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-bsblan"
3
- version = "6.1.11"
3
+ version = "6.2.0"
4
4
  description = "Asynchronous Python client for BSBLAN API"
5
5
  authors = [
6
6
  {name = "Willem-Jan van Rootselaar", email = "liudgervr@gmail.com"}
@@ -197,7 +197,7 @@ dev = [
197
197
  # hatch is required to support type hinting and proper packaging of the py.typed file.
198
198
  "hatch>=1.14.1",
199
199
  "isort==8.0.1",
200
- "ty==0.0.89",
200
+ "ty==0.0.90",
201
201
  "prek>=0.3.3",
202
202
  "pre-commit-hooks==6.0.0",
203
203
  "pylint==4.1.3",
@@ -51,9 +51,10 @@ from .models import (
51
51
  State,
52
52
  StaticState,
53
53
  )
54
- from .utility import is_param_value_active, validate_time_format
54
+ from .utility import is_param_value_active
55
55
 
56
56
  if TYPE_CHECKING:
57
+ from datetime import datetime
57
58
  from typing import Self
58
59
 
59
60
  from aiohttp.client import ClientSession
@@ -862,24 +863,31 @@ class BSBLAN:
862
863
  time_data = {"time": data["0"]}
863
864
  return DeviceTime.model_validate(time_data)
864
865
 
865
- async def set_time(self, time_value: str) -> None:
866
+ async def set_time(self, value: datetime) -> None:
866
867
  """Set the time on the BSB-LAN device.
867
868
 
868
869
  Args:
869
- time_value (str): The time value to set in format "DD.MM.YYYY HH:MM:SS"
870
- (e.g., "13.08.2025 10:25:55").
870
+ value (datetime): The local time of the heating system. Its date and
871
+ time are written as they are, without timezone conversion.
871
872
 
872
873
  Raises:
873
- BSBLANInvalidParameterError: If the time format is invalid.
874
+ BSBLANInvalidParameterError: If the year is out of range.
875
+ BSBLANError: If the device does not accept the new time.
874
876
 
875
877
  """
876
878
  await self._ensure_device_metadata()
877
879
  self._validate_time_sync_supported()
878
- self._validate_time_format(time_value)
880
+ if not Validation.MIN_YEAR <= value.year <= Validation.MAX_YEAR:
881
+ msg = f"Invalid year: {value.year}"
882
+ raise BSBLANInvalidParameterError(msg)
883
+ # BSB-LAN 4.1 rejects a space between the date and the time
879
884
  response = await self._request(
880
- base_path="/JS", data=self._set_payload("0", time_value)
885
+ base_path="/JS",
886
+ data=self._set_payload("0", value.strftime("%d.%m.%Y_%H:%M:%S")),
881
887
  )
882
888
  logger.debug("Response for setting time: %s", response)
889
+ if response.get("0", {}).get("status") != 1:
890
+ raise BSBLANError(ErrorMsg.TIME_SYNC_FAILED)
883
891
 
884
892
  async def thermostat( # noqa: PLR0913 # pylint: disable=too-many-arguments
885
893
  self,
@@ -992,21 +1000,6 @@ class BSBLAN:
992
1000
  circuit,
993
1001
  )
994
1002
 
995
- def _validate_time_format(self, time_value: str) -> None:
996
- """Validate the time format.
997
-
998
- Args:
999
- time_value (str): The time value to validate.
1000
-
1001
- Raises:
1002
- BSBLANInvalidParameterError: If the time format is invalid.
1003
-
1004
- """
1005
- try:
1006
- validate_time_format(time_value, Validation.MIN_YEAR, Validation.MAX_YEAR)
1007
- except ValueError as err:
1008
- raise BSBLANInvalidParameterError(str(err)) from err
1009
-
1010
1003
  def _set_payload(
1011
1004
  self, parameter: str, value: str, type_: str = "1"
1012
1005
  ) -> dict[str, Any]:
@@ -561,6 +561,7 @@ class ErrorMsg:
561
561
  )
562
562
  NO_HEATING_SCHEDULE_PARAMS = "No heating schedule parameters available"
563
563
  TIME_SYNC_NOT_SUPPORTED = "Time synchronization is not supported by this device"
564
+ TIME_SYNC_FAILED = "The device did not accept the new time"
564
565
  BUS_WRITE_NOT_SUPPORTED = "Writing parameters is not supported by this device"
565
566
 
566
567
 
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- import re
7
6
  from dataclasses import dataclass, field
8
7
  from typing import Any
9
8
 
@@ -12,65 +11,6 @@ from .constants import CircuitConfig
12
11
  logger = logging.getLogger(__name__)
13
12
 
14
13
 
15
- def validate_time_format(
16
- time_value: str,
17
- min_year: int,
18
- max_year: int,
19
- ) -> None:
20
- """Validate the BSB-LAN time format.
21
-
22
- Args:
23
- time_value: The time value to validate in format DD.MM.YYYY HH:MM:SS.
24
- min_year: Minimum valid year.
25
- max_year: Maximum valid year.
26
-
27
- Raises:
28
- ValueError: If the time format is invalid.
29
-
30
- """
31
- # BSB-LAN supports format: DD.MM.YYYY HH:MM:SS (e.g., "13.08.2025 10:25:55")
32
- pattern = r"^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})$"
33
-
34
- match = re.match(pattern, time_value)
35
- if not match:
36
- msg = f"Invalid time format: {time_value}. Expected DD.MM.YYYY HH:MM:SS"
37
- raise ValueError(msg)
38
-
39
- day, month, year, hour, minute, second = map(int, match.groups())
40
-
41
- # Validate ranges
42
- if not (1 <= day <= 31):
43
- msg = f"Invalid day: {day}"
44
- raise ValueError(msg)
45
- if not (1 <= month <= 12):
46
- msg = f"Invalid month: {month}"
47
- raise ValueError(msg)
48
- if not (min_year <= year <= max_year):
49
- msg = f"Invalid year: {year}"
50
- raise ValueError(msg)
51
- if not (0 <= hour <= 23):
52
- msg = f"Invalid hour: {hour}"
53
- raise ValueError(msg)
54
- if not (0 <= minute <= 59):
55
- msg = f"Invalid minute: {minute}"
56
- raise ValueError(msg)
57
- if not (0 <= second <= 59):
58
- msg = f"Invalid second: {second}"
59
- raise ValueError(msg)
60
-
61
- # Additional validation for days per month
62
- if month in (4, 6, 9, 11) and day > 30:
63
- msg = f"Invalid day {day} for month {month}"
64
- raise ValueError(msg)
65
- if month == 2:
66
- # Leap year check
67
- is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
68
- max_day = 29 if is_leap else 28
69
- if day > max_day:
70
- msg = f"Invalid day {day} for February in year {year}"
71
- raise ValueError(msg)
72
-
73
-
74
14
  def is_param_value_active(param: dict[str, Any] | None) -> bool:
75
15
  """Check whether a parameter payload carries an active value.
76
16
 
@@ -5,6 +5,7 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import json
8
+ from datetime import UTC, datetime
8
9
  from typing import TYPE_CHECKING, Any
9
10
  from unittest.mock import AsyncMock
10
11
 
@@ -126,7 +127,7 @@ async def test_pps_set_time_raises_without_posting(pps_bsblan: BSBLAN) -> None:
126
127
  pps_bsblan._request = request_mock # type: ignore[method-assign]
127
128
 
128
129
  with pytest.raises(BSBLANError, match=ErrorMsg.TIME_SYNC_NOT_SUPPORTED):
129
- await pps_bsblan.set_time("01.01.2024 12:30:45")
130
+ await pps_bsblan.set_time(datetime(2024, 1, 1, 12, 30, 45, tzinfo=UTC))
130
131
 
131
132
  request_mock.assert_not_awaited()
132
133
 
@@ -178,7 +179,7 @@ async def test_set_time_fetches_device_metadata_before_sync() -> None:
178
179
  bsblan._request = request_mock # type: ignore[method-assign]
179
180
 
180
181
  with pytest.raises(BSBLANError, match=ErrorMsg.TIME_SYNC_NOT_SUPPORTED):
181
- await bsblan.set_time("01.01.2024 12:30:45")
182
+ await bsblan.set_time(datetime(2024, 1, 1, 12, 30, 45, tzinfo=UTC))
182
183
 
183
184
  request_mock.assert_awaited_once_with(base_path="/JI")
184
185
 
@@ -0,0 +1,112 @@
1
+ """Tests for device time functionality."""
2
+
3
+ # pylint: disable=duplicate-code
4
+ # pylint: disable=protected-access
5
+ # file deepcode ignore W0212: this is a testfile
6
+
7
+ import json
8
+ from datetime import UTC, datetime, timedelta, timezone
9
+ from unittest.mock import AsyncMock
10
+
11
+ import pytest
12
+
13
+ from bsblan import BSBLAN
14
+ from bsblan.constants import ErrorMsg
15
+ from bsblan.exceptions import BSBLANError, BSBLANInvalidParameterError
16
+ from bsblan.models import Device, DeviceTime, EntityInfo
17
+ from tests import load_fixture
18
+
19
+
20
+ def set_bsb_device_metadata(bsblan: BSBLAN) -> None:
21
+ """Seed cached BSB device metadata for time tests."""
22
+ bsblan._device = Device.model_validate(json.loads(load_fixture("device.json")))
23
+
24
+
25
+ def set_time_status(bsblan: BSBLAN, status: int) -> None:
26
+ """Make BSB-LAN answer a time write with the given status."""
27
+ assert isinstance(bsblan._request, AsyncMock)
28
+ bsblan._request.return_value = {"0": {"status": status}}
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_get_time(mock_bsblan: BSBLAN) -> None:
33
+ """Test getting device time.
34
+
35
+ Args:
36
+ mock_bsblan (BSBLAN): The mock BSBLAN instance.
37
+
38
+ """
39
+ # Load the time response from fixture
40
+ time_response = json.loads(load_fixture("time.json"))
41
+
42
+ assert isinstance(mock_bsblan._request, AsyncMock)
43
+ mock_bsblan._request.return_value = time_response
44
+ set_bsb_device_metadata(mock_bsblan)
45
+
46
+ # Test getting device time
47
+ device_time = await mock_bsblan.time()
48
+
49
+ # Verify the request was made correctly
50
+ mock_bsblan._request.assert_awaited_with(params={"Parameter": "0"})
51
+
52
+ # Verify the response structure
53
+ assert isinstance(device_time, DeviceTime)
54
+ assert isinstance(device_time.time, EntityInfo)
55
+ assert device_time.time.name == "Current date / Current time of day"
56
+ assert device_time.time.value == "13.08.2025 11:51:37"
57
+ assert device_time.time.desc == ""
58
+ assert device_time.time.data_type == 5
59
+
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_set_time(mock_bsblan: BSBLAN) -> None:
63
+ """Test setting device time."""
64
+ set_bsb_device_metadata(mock_bsblan)
65
+ set_time_status(mock_bsblan, 1)
66
+
67
+ await mock_bsblan.set_time(datetime(2024, 1, 1, 12, 30, 45, tzinfo=UTC))
68
+
69
+ assert isinstance(mock_bsblan._request, AsyncMock)
70
+ mock_bsblan._request.assert_awaited_with(
71
+ base_path="/JS",
72
+ data={"Parameter": "0", "Value": "01.01.2024_12:30:45", "Type": "1"},
73
+ )
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_set_time_without_timezone_conversion(mock_bsblan: BSBLAN) -> None:
78
+ """Test the date and time are written as they are in the given timezone."""
79
+ set_bsb_device_metadata(mock_bsblan)
80
+ set_time_status(mock_bsblan, 1)
81
+ utc_plus_two = timezone(timedelta(hours=2))
82
+
83
+ await mock_bsblan.set_time(datetime(2025, 8, 13, 10, 25, 55, tzinfo=utc_plus_two))
84
+
85
+ assert isinstance(mock_bsblan._request, AsyncMock)
86
+ mock_bsblan._request.assert_awaited_with(
87
+ base_path="/JS",
88
+ data={"Parameter": "0", "Value": "13.08.2025_10:25:55", "Type": "1"},
89
+ )
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_set_time_rejected(mock_bsblan: BSBLAN) -> None:
94
+ """Test setting device time raises when BSB-LAN rejects it."""
95
+ set_bsb_device_metadata(mock_bsblan)
96
+ set_time_status(mock_bsblan, 0)
97
+
98
+ with pytest.raises(BSBLANError, match=ErrorMsg.TIME_SYNC_FAILED):
99
+ await mock_bsblan.set_time(datetime(2024, 1, 1, 12, 30, 45, tzinfo=UTC))
100
+
101
+
102
+ @pytest.mark.asyncio
103
+ @pytest.mark.parametrize("year", [1899, 2101])
104
+ async def test_set_time_invalid_year(mock_bsblan: BSBLAN, year: int) -> None:
105
+ """Test setting device time outside the supported years raises."""
106
+ set_bsb_device_metadata(mock_bsblan)
107
+
108
+ with pytest.raises(BSBLANInvalidParameterError, match=f"Invalid year: {year}"):
109
+ await mock_bsblan.set_time(datetime(year, 1, 1, 12, 30, 45, tzinfo=UTC))
110
+
111
+ assert isinstance(mock_bsblan._request, AsyncMock)
112
+ mock_bsblan._request.assert_not_awaited()
@@ -0,0 +1,173 @@
1
+ """Test cases for utility module."""
2
+
3
+ # pylint: disable=redefined-outer-name
4
+ # pylint: disable=protected-access
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ import pytest
12
+
13
+ from bsblan.utility import APIValidator, is_param_value_active
14
+
15
+
16
+ @pytest.fixture
17
+ def mock_request_data() -> dict[str, Any]:
18
+ """Fixture for mock request data."""
19
+ return {
20
+ "700": {
21
+ "name": "Operating mode",
22
+ "error": 0,
23
+ "value": "3",
24
+ "desc": "Comfort",
25
+ "dataType": 1,
26
+ "readonly": 0,
27
+ "unit": "",
28
+ },
29
+ "710": {
30
+ "name": "Comfort setpoint",
31
+ "error": 0,
32
+ "value": "20.0",
33
+ "desc": "",
34
+ "dataType": 0,
35
+ "readonly": 0,
36
+ "unit": "°C",
37
+ },
38
+ }
39
+
40
+
41
+ @pytest.fixture
42
+ def validator() -> APIValidator:
43
+ """Fixture for APIValidator instance."""
44
+ api_config = {
45
+ "heating": {
46
+ "700": "hvac_mode",
47
+ "710": "target_temperature",
48
+ # Add other parameters as needed
49
+ },
50
+ "staticValues": {
51
+ "712": "min_temp",
52
+ "714": "heating_protective_setpoint",
53
+ "716": "max_temp",
54
+ # Add other parameters as needed
55
+ },
56
+ "hot_water": {
57
+ "8830": "dhw_actual_value_top_temperature",
58
+ "8820": "state_dhw_pump",
59
+ # Add other parameters as needed
60
+ },
61
+ }
62
+ validated_sections: set[str] = set()
63
+ return APIValidator(api_config=api_config, validated_sections=validated_sections)
64
+
65
+
66
+ def test_is_param_value_active() -> None:
67
+ """Test the shared active-value predicate."""
68
+ assert is_param_value_active({"value": "20.0"}) is True
69
+
70
+ assert is_param_value_active({"value": None}) is False
71
+ assert is_param_value_active({"value": "---"}) is False
72
+ assert is_param_value_active({}) is False
73
+ assert is_param_value_active(None) is False
74
+
75
+
76
+ def test_is_valid_param(validator: APIValidator) -> None:
77
+ """Test parameter validation logic."""
78
+ valid_param: dict[str, Any] = {
79
+ "value": "20.0",
80
+ }
81
+ assert validator._is_valid_param(valid_param) is True
82
+
83
+ invalid_params: list[dict[str, Any]] = [
84
+ {"value": None}, # None value
85
+ {"value": "---"}, # Invalid value
86
+ {}, # Empty dict
87
+ ]
88
+
89
+ for param in invalid_params:
90
+ assert validator._is_valid_param(param) is False
91
+
92
+
93
+ def test_get_section_params(validator: APIValidator) -> None:
94
+ """Test getting section parameters."""
95
+ # Test existing section
96
+ heating_params = validator.get_section_params("heating")
97
+ assert isinstance(heating_params, dict)
98
+ assert "700" in heating_params
99
+ assert heating_params["700"] == "hvac_mode"
100
+
101
+ # Test non-existent section
102
+ empty_params = validator.get_section_params("non_existent")
103
+ assert isinstance(empty_params, dict)
104
+ assert len(empty_params) == 0
105
+
106
+
107
+ def test_is_section_validated(validator: APIValidator) -> None:
108
+ """Test section validation status checking."""
109
+ assert validator.is_section_validated("heating") is False
110
+
111
+ validator.validated_sections.add("heating")
112
+ assert validator.is_section_validated("heating") is True
113
+
114
+
115
+ def test_reset_validation(validator: APIValidator) -> None:
116
+ """Test validation reset functionality."""
117
+ # Add some validated sections
118
+ validator.validated_sections.update({"heating", "sensor"})
119
+ validator.reset_validation()
120
+ assert len(validator.validated_sections) == 0
121
+
122
+
123
+ def test_api_validator_with_empty_sections() -> None:
124
+ """Test APIValidator with empty API config sections."""
125
+ empty_config: dict[str, dict[str, Any]] = {
126
+ "heating": {},
127
+ "sensor": {},
128
+ "device": {},
129
+ "staticValues": {},
130
+ "hot_water": {},
131
+ }
132
+ validator = APIValidator(empty_config)
133
+
134
+ assert validator.get_section_params("heating") == {}
135
+ assert not validator.is_section_validated("heating")
136
+
137
+
138
+ def test_api_validator_error_handling(
139
+ validator: APIValidator,
140
+ caplog: pytest.LogCaptureFixture,
141
+ ) -> None:
142
+ """Test error handling in APIValidator."""
143
+ with caplog.at_level(logging.INFO):
144
+ # Test with invalid request data
145
+ validator.validate_section("heating", {})
146
+ assert "Parameter" in caplog.text
147
+
148
+
149
+ def test_api_validator_logging(
150
+ validator: APIValidator,
151
+ mock_request_data: dict[str, Any],
152
+ caplog: pytest.LogCaptureFixture,
153
+ ) -> None:
154
+ """Test logging functionality in APIValidator."""
155
+ with caplog.at_level(logging.DEBUG):
156
+ validator.validate_section("heating", mock_request_data)
157
+
158
+ # Verify debug logs
159
+ assert "Validated section 'heating'" in caplog.text
160
+ assert "removed" in caplog.text
161
+
162
+
163
+ def test_api_validator_concurrent_validation(
164
+ validator: APIValidator,
165
+ mock_request_data: dict[str, Any],
166
+ ) -> None:
167
+ """Test concurrent validation of multiple sections."""
168
+ # Simulate concurrent validation attempts
169
+ validator.validate_section("heating", mock_request_data)
170
+ validator.validate_section("heating", mock_request_data)
171
+
172
+ assert len(validator.validated_sections) == 1
173
+ assert "heating" in validator.validated_sections
@@ -1 +0,0 @@
1
- 24.22.0