python-bsblan 5.1.5__tar.gz → 5.2.1__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 (120) hide show
  1. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/codeql.yaml +2 -2
  2. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/docs.yaml +1 -1
  3. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/linting.yaml +2 -2
  4. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/release-drafter.yaml +1 -1
  5. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/release.yaml +1 -1
  6. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/scorecard.yml +1 -1
  7. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/tests.yaml +3 -3
  8. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/typing.yaml +1 -1
  9. python_bsblan-5.2.1/.nvmrc +1 -0
  10. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/PKG-INFO +1 -1
  11. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/api/client.md +2 -0
  12. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/api/models.md +8 -0
  13. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/pyproject.toml +3 -3
  14. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/__init__.py +4 -0
  15. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/bsblan.py +104 -42
  16. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/constants.py +28 -0
  17. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/models.py +61 -19
  18. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_circuit.py +49 -120
  19. python_bsblan-5.2.1/tests/test_heating_schedule.py +145 -0
  20. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_schedule_models.py +32 -1
  21. python_bsblan-5.2.1/tests/test_set_heating_schedule.py +150 -0
  22. python_bsblan-5.1.5/.nvmrc +0 -1
  23. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.editorconfig +0 -0
  24. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.gitattributes +0 -0
  25. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/CODE_OF_CONDUCT.md +0 -0
  26. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/CONTRIBUTING.md +0 -0
  27. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/SECURITY.md +0 -0
  31. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/copilot-instructions.md +0 -0
  32. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/labels.yml +0 -0
  33. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/prompts/add-parameter.prompt.md +0 -0
  34. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/prompts/code-review.prompt.md +0 -0
  35. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/release-drafter.yml +0 -0
  36. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/renovate.json +0 -0
  37. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/skills/bsblan-parameters/SKILL.md +0 -0
  38. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/skills/bsblan-testing/SKILL.md +0 -0
  39. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/auto-approve-renovate.yml +0 -0
  40. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/dependency-review.yaml +0 -0
  41. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/labels.yaml +0 -0
  42. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/lock.yaml +0 -0
  43. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/pr-labels.yaml +0 -0
  44. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/workflows/stale.yaml +0 -0
  45. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.github/zizmor.yml +0 -0
  46. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.gitignore +0 -0
  47. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.pre-commit-config.yaml +0 -0
  48. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.prettierignore +0 -0
  49. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/.yamllint +0 -0
  50. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/AGENTS.md +0 -0
  51. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/CLAUDE.md +0 -0
  52. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/LICENSE.md +0 -0
  53. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/Makefile +0 -0
  54. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/README.md +0 -0
  55. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/api/constants.md +0 -0
  56. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/api/exceptions.md +0 -0
  57. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/getting-started.md +0 -0
  58. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/docs/index.md +0 -0
  59. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/control.py +0 -0
  60. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/discovery.py +0 -0
  61. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/fetch_param.py +0 -0
  62. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/profile_init.py +0 -0
  63. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/ruff.toml +0 -0
  64. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/examples/speed_test.py +0 -0
  65. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/mkdocs.yml +0 -0
  66. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/package-lock.json +0 -0
  67. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/package.json +0 -0
  68. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/sonar-project.properties +0 -0
  69. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/exceptions.py +0 -0
  70. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/py.typed +0 -0
  71. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/src/bsblan/utility.py +0 -0
  72. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/__init__.py +0 -0
  73. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/conftest.py +0 -0
  74. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/device.json +0 -0
  75. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/dict_version.json +0 -0
  76. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/hot_water_state.json +0 -0
  77. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/info.json +0 -0
  78. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/password.txt +0 -0
  79. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/sensor.json +0 -0
  80. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/state.json +0 -0
  81. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/state_circuit2.json +0 -0
  82. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/static_state.json +0 -0
  83. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/static_state_circuit2.json +0 -0
  84. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/thermostat_hvac.json +0 -0
  85. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/thermostat_temp.json +0 -0
  86. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/fixtures/time.json +0 -0
  87. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/ruff.toml +0 -0
  88. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_api_initialization.py +0 -0
  89. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_api_validation.py +0 -0
  90. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_auth.py +0 -0
  91. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_backoff_retry.py +0 -0
  92. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_bsblan.py +0 -0
  93. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_bsblan_edge_cases.py +0 -0
  94. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_configuration.py +0 -0
  95. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_constants.py +0 -0
  96. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_context_manager.py +0 -0
  97. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_device.py +0 -0
  98. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_dhw_time_switch.py +0 -0
  99. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_entity_info.py +0 -0
  100. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_entity_info_ha.py +0 -0
  101. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_hot_water_additional.py +0 -0
  102. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_hotwater_state.py +0 -0
  103. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_include_parameter.py +0 -0
  104. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_info.py +0 -0
  105. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_initialization.py +0 -0
  106. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_read_parameters.py +0 -0
  107. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_reset_validation.py +0 -0
  108. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_sensor.py +0 -0
  109. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_set_hot_water_schedule.py +0 -0
  110. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_set_hotwater.py +0 -0
  111. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_state.py +0 -0
  112. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_static_state.py +0 -0
  113. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_temperature_unit.py +0 -0
  114. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_temperature_validation.py +0 -0
  115. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_thermostat.py +0 -0
  116. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_time.py +0 -0
  117. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_utility.py +0 -0
  118. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_utility_additional.py +0 -0
  119. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/tests/test_utility_edge_cases.py +0 -0
  120. {python_bsblan-5.1.5 → python_bsblan-5.2.1}/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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
30
+ uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
31
31
  - name: 🚀 Perform CodeQL Analysis
32
- uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
32
+ uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
@@ -27,7 +27,7 @@ jobs:
27
27
  with:
28
28
  persist-credentials: false
29
29
  - name: 🏗 Set up uv
30
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
30
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
31
31
  with:
32
32
  enable-cache: true
33
33
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  persist-credentials: false
27
27
  - name: 🏗 Set up uv
28
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
28
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
29
29
  with:
30
30
  enable-cache: true
31
31
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -49,7 +49,7 @@ jobs:
49
49
  with:
50
50
  persist-credentials: false
51
51
  - name: 🏗 Set up uv
52
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
52
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
53
53
  with:
54
54
  enable-cache: true
55
55
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -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@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
39
+ uses: release-drafter/release-drafter@563bf132657a13ded0b01fcb723c5a58cdd824e2 # v7.2.1
40
40
  with:
41
41
  prerelease: ${{ github.event.inputs.prerelease == 'true' }}
42
42
  prerelease-identifier: ${{ github.event.inputs.prerelease_identifier }}
@@ -29,7 +29,7 @@ jobs:
29
29
  with:
30
30
  persist-credentials: false
31
31
  - name: 🏗 Set up uv
32
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
32
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
33
33
  with:
34
34
  enable-cache: false
35
35
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
69
+ uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
70
70
  with:
71
71
  sarif_file: results.sarif
@@ -33,7 +33,7 @@ jobs:
33
33
  with:
34
34
  persist-credentials: false
35
35
  - name: 🏗 Set up uv
36
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
36
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
37
37
  with:
38
38
  enable-cache: true
39
39
  - name: 🏗 Set up Python ${{ matrix.python }}
@@ -68,7 +68,7 @@ jobs:
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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
71
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
72
72
  with:
73
73
  enable-cache: true
74
74
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -93,7 +93,7 @@ jobs:
93
93
  - name: SonarQube Cloud Scan
94
94
  if: env.HAS_SONAR_TOKEN == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork)
95
95
  # yamllint disable-line rule:line-length
96
- uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 # v7.1.0
96
+ uses: SonarSource/sonarqube-scan-action@59db25f34e16620e48ab4bb9e4a5dce155cb5432 # v8.0
97
97
  env:
98
98
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
99
99
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -27,7 +27,7 @@ jobs:
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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
30
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
31
31
  with:
32
32
  enable-cache: true
33
33
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
@@ -0,0 +1 @@
1
+ 24.15.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-bsblan
3
- Version: 5.1.5
3
+ Version: 5.2.1
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
@@ -19,6 +19,8 @@ The main entry point for interacting with BSB-LAN devices.
19
19
  - time
20
20
  - set_time
21
21
  - thermostat
22
+ - heating_schedule
23
+ - set_heating_schedule
22
24
  - hot_water_state
23
25
  - hot_water_config
24
26
  - hot_water_schedule
@@ -26,6 +26,10 @@ Data models for BSB-LAN API responses.
26
26
 
27
27
  ::: bsblan.Sensor
28
28
 
29
+ ### HeatingTimeSwitchPrograms
30
+
31
+ ::: bsblan.HeatingTimeSwitchPrograms
32
+
29
33
  ## Hot water
30
34
 
31
35
  ### HotWaterState
@@ -46,6 +50,10 @@ Data models for BSB-LAN API responses.
46
50
 
47
51
  ## Schedules
48
52
 
53
+ ### HeatingSchedule
54
+
55
+ ::: bsblan.HeatingSchedule
56
+
49
57
  ### DHWSchedule
50
58
 
51
59
  ::: bsblan.DHWSchedule
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-bsblan"
3
- version = "5.1.5"
3
+ version = "5.2.1"
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.32",
199
+ "ty==0.0.34",
200
200
  "prek>=0.3.3",
201
201
  "pre-commit-hooks==6.0.0",
202
202
  "pylint==4.0.5",
@@ -205,7 +205,7 @@ dev = [
205
205
  "pytest-cov==7.1.0",
206
206
  "pytest-xdist>=3.8.0",
207
207
  "pyupgrade==3.21.2",
208
- "ruff==0.15.11",
208
+ "ruff==0.15.12",
209
209
  "safety==3.7.0",
210
210
  "vulture==2.16",
211
211
  "yamllint==1.38.0",
@@ -17,6 +17,8 @@ from .models import (
17
17
  DHWTimeSwitchPrograms,
18
18
  EntityInfo,
19
19
  EntityValue,
20
+ HeatingSchedule,
21
+ HeatingTimeSwitchPrograms,
20
22
  HotWaterConfig,
21
23
  HotWaterSchedule,
22
24
  HotWaterState,
@@ -45,6 +47,8 @@ __all__ = [
45
47
  "EntityValue",
46
48
  "HVACActionCategory",
47
49
  "HeatingCircuitStatus",
50
+ "HeatingSchedule",
51
+ "HeatingTimeSwitchPrograms",
48
52
  "HotWaterConfig",
49
53
  "HotWaterSchedule",
50
54
  "HotWaterState",
@@ -22,6 +22,7 @@ from .constants import (
22
22
  APIConfig,
23
23
  CircuitConfig,
24
24
  ErrorMsg,
25
+ HeatingScheduleParams,
25
26
  HotWaterParams,
26
27
  Validation,
27
28
  )
@@ -38,6 +39,8 @@ from .models import (
38
39
  DeviceTime,
39
40
  DHWSchedule,
40
41
  EntityInfo,
42
+ HeatingSchedule,
43
+ HeatingTimeSwitchPrograms,
41
44
  HotWaterConfig,
42
45
  HotWaterSchedule,
43
46
  HotWaterState,
@@ -149,13 +152,10 @@ class BSBLAN:
149
152
  async def get_available_circuits(self) -> list[int]:
150
153
  """Detect which heating circuits are available on the device.
151
154
 
152
- Uses a two-step probe for each circuit (1, 2):
153
- 1. Query the operating mode parameter — the response must be
154
- non-empty and contain actual data.
155
- 2. Query the status parameter (8000/8001) an inactive
156
- circuit returns ``value="0"`` with ``desc="---"``.
157
-
158
- A circuit is only considered available when both checks pass.
155
+ Uses the configured operating mode probe parameters from
156
+ CircuitConfig.PROBE_PARAMS as the only discovery signal. Status
157
+ parameters are not queried during discovery to keep setup lightweight
158
+ and avoid excluding valid circuits when status data is unavailable.
159
159
 
160
160
  This is useful for integration setup flows (e.g., Home Assistant
161
161
  config flow) to discover how many circuits the user's controller
@@ -176,46 +176,23 @@ class BSBLAN:
176
176
  response = await self._request(
177
177
  params={"Parameter": param_id},
178
178
  )
179
- # A circuit exists if the response contains the param_id key
180
- # with actual data (not an empty dict)
181
- if not response.get(param_id):
182
- continue
183
-
184
- # Secondary check: query the status parameter.
185
- # Inactive circuits either:
186
- # - return value="0" and desc="---"
187
- # - return an empty dict {} (param not supported)
188
- status_id = CircuitConfig.STATUS_PARAMS[circuit]
189
- status_resp = await self._request(
190
- params={"Parameter": status_id},
179
+ except BSBLANError:
180
+ logger.debug(
181
+ "Circuit %d not available (operating mode request failed)",
182
+ circuit,
191
183
  )
192
- status_data = status_resp.get(status_id, {})
193
-
194
- # Empty response means the parameter doesn't exist
195
- if not status_data or not isinstance(status_data, dict):
196
- logger.debug(
197
- "Circuit %d has no status data (not supported)",
198
- circuit,
199
- )
200
- continue
184
+ continue
201
185
 
202
- # value="0" + desc="---" means inactive
203
- if (
204
- status_data.get("desc") == CircuitConfig.INACTIVE_MARKER
205
- and str(status_data.get("value", "")) == "0"
206
- ):
207
- logger.debug(
208
- "Circuit %d has status '---' (inactive)",
209
- circuit,
210
- )
211
- continue
212
-
213
- available.append(circuit)
214
- except BSBLANError:
186
+ # A circuit exists if the response contains the operating mode key
187
+ # with actual data (not an empty dict).
188
+ if not response.get(param_id):
215
189
  logger.debug(
216
- "Circuit %d not available (request failed)",
190
+ "Circuit %d has no operating mode data (not supported)",
217
191
  circuit,
218
192
  )
193
+ continue
194
+
195
+ available.append(circuit)
219
196
  return sorted(available)
220
197
 
221
198
  async def _setup_api_validator(self) -> None:
@@ -1380,6 +1357,91 @@ class BSBLAN:
1380
1357
  include=include,
1381
1358
  )
1382
1359
 
1360
+ async def heating_schedule(
1361
+ self,
1362
+ include: list[str] | None = None,
1363
+ circuit: int = 1,
1364
+ ) -> HeatingTimeSwitchPrograms:
1365
+ """Get heating time switch programs for a specific circuit.
1366
+
1367
+ Args:
1368
+ include: Optional list of day names to fetch. If None,
1369
+ fetches all schedule parameters. Valid names include:
1370
+ monday, tuesday, wednesday, thursday,
1371
+ friday, saturday, sunday, standard_values.
1372
+ circuit: The heating circuit number (1 or 2). Defaults to 1.
1373
+
1374
+ Returns:
1375
+ HeatingTimeSwitchPrograms: Heating schedule information.
1376
+
1377
+ """
1378
+ self._validate_circuit(circuit)
1379
+ time_program_params = HeatingScheduleParams.TIME_PROGRAMS[circuit]
1380
+
1381
+ filtered_params = time_program_params
1382
+ if include is not None:
1383
+ if not include:
1384
+ raise BSBLANError(ErrorMsg.EMPTY_INCLUDE_LIST)
1385
+ filtered_params = {
1386
+ param_id: name
1387
+ for param_id, name in time_program_params.items()
1388
+ if name in include
1389
+ }
1390
+ if not filtered_params:
1391
+ raise BSBLANError(ErrorMsg.INVALID_INCLUDE_PARAMS)
1392
+
1393
+ params = self._extract_params_summary(filtered_params)
1394
+ data = await self._request(params={"Parameter": params["string_par"]})
1395
+ mapped_data = {
1396
+ name: data[param_id]
1397
+ for param_id, name in filtered_params.items()
1398
+ if param_id in data
1399
+ }
1400
+
1401
+ if not mapped_data:
1402
+ raise BSBLANError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
1403
+
1404
+ return HeatingTimeSwitchPrograms.model_validate(mapped_data)
1405
+
1406
+ async def set_heating_schedule(
1407
+ self,
1408
+ schedule: HeatingSchedule,
1409
+ circuit: int = 1,
1410
+ ) -> None:
1411
+ """Set heating time switch programs for a specific circuit.
1412
+
1413
+ This method allows setting weekly heating schedules using a type-safe
1414
+ interface with TimeSlot and DaySchedule objects.
1415
+
1416
+ Args:
1417
+ schedule: HeatingSchedule object containing the weekly schedule.
1418
+ circuit: The heating circuit number (1 or 2). Defaults to 1.
1419
+
1420
+ Raises:
1421
+ BSBLANError: If no schedule is provided.
1422
+
1423
+ """
1424
+ self._validate_circuit(circuit)
1425
+
1426
+ if not schedule.has_any_schedule():
1427
+ raise BSBLANError(ErrorMsg.NO_SCHEDULE)
1428
+
1429
+ day_param_map = {
1430
+ v: k
1431
+ for k, v in HeatingScheduleParams.TIME_PROGRAMS[circuit].items()
1432
+ if v != "standard_values"
1433
+ }
1434
+
1435
+ for day_name, param_id in day_param_map.items():
1436
+ day_schedule: DaySchedule | None = getattr(schedule, day_name)
1437
+ if day_schedule is not None:
1438
+ state = {
1439
+ "Parameter": param_id,
1440
+ "Value": day_schedule.to_bsblan_format(),
1441
+ "Type": "1",
1442
+ }
1443
+ await self._set_device_state(state)
1444
+
1383
1445
  async def set_hot_water(self, params: SetHotWaterParam) -> None:
1384
1446
  """Change the state of the hot water system through BSB-Lan.
1385
1447
 
@@ -491,6 +491,7 @@ class ErrorMsg:
491
491
  EMPTY_INCLUDE_LIST = (
492
492
  "Empty include list provided. Use None to fetch all parameters."
493
493
  )
494
+ NO_HEATING_SCHEDULE_PARAMS = "No heating schedule parameters available"
494
495
 
495
496
 
496
497
  # Handle both ASCII and Unicode degree symbols
@@ -655,3 +656,30 @@ class HotWaterParams:
655
656
  "567": "sunday",
656
657
  "576": "standard_values",
657
658
  }
659
+
660
+
661
+ class HeatingScheduleParams:
662
+ """Heating schedule parameter mappings per circuit."""
663
+
664
+ TIME_PROGRAMS: Final[dict[int, dict[str, str]]] = {
665
+ 1: {
666
+ "501": "monday",
667
+ "502": "tuesday",
668
+ "503": "wednesday",
669
+ "504": "thursday",
670
+ "505": "friday",
671
+ "506": "saturday",
672
+ "507": "sunday",
673
+ "516": "standard_values",
674
+ },
675
+ 2: {
676
+ "521": "monday",
677
+ "522": "tuesday",
678
+ "523": "wednesday",
679
+ "524": "thursday",
680
+ "525": "friday",
681
+ "526": "saturday",
682
+ "527": "sunday",
683
+ "536": "standard_values",
684
+ },
685
+ }
@@ -141,23 +141,10 @@ class DaySchedule:
141
141
 
142
142
 
143
143
  @dataclass
144
- class DHWSchedule:
145
- """Weekly hot water schedule for setting time programs.
144
+ class WeeklySchedule:
145
+ """Base weekly schedule with optional day schedules.
146
146
 
147
- Use this dataclass to set DHW time programs via set_hot_water_schedule().
148
- Each day can have up to 3 time slots.
149
-
150
- Example:
151
- >>> schedule = DHWSchedule(
152
- ... monday=DaySchedule(slots=[
153
- ... TimeSlot(time(6, 0), time(8, 0)),
154
- ... TimeSlot(time(17, 0), time(21, 0)),
155
- ... ]),
156
- ... tuesday=DaySchedule(slots=[
157
- ... TimeSlot(time(6, 0), time(8, 0)),
158
- ... ])
159
- ... )
160
- >>> await client.set_hot_water_schedule(schedule)
147
+ Each day can have up to 3 time slots (validated by DaySchedule).
161
148
 
162
149
  """
163
150
 
@@ -190,6 +177,38 @@ class DHWSchedule:
190
177
  )
191
178
 
192
179
 
180
+ @dataclass
181
+ class DHWSchedule(WeeklySchedule):
182
+ """Weekly hot water schedule for setting time programs.
183
+
184
+ Use this dataclass to set DHW time programs via set_hot_water_schedule().
185
+ Each day can have up to 3 time slots.
186
+
187
+ Example:
188
+ >>> schedule = DHWSchedule(
189
+ ... monday=DaySchedule(slots=[
190
+ ... TimeSlot(time(6, 0), time(8, 0)),
191
+ ... TimeSlot(time(17, 0), time(21, 0)),
192
+ ... ]),
193
+ ... tuesday=DaySchedule(slots=[
194
+ ... TimeSlot(time(6, 0), time(8, 0)),
195
+ ... ])
196
+ ... )
197
+ >>> await client.set_hot_water_schedule(schedule)
198
+
199
+ """
200
+
201
+
202
+ @dataclass
203
+ class HeatingSchedule(WeeklySchedule):
204
+ """Weekly heating schedule for setting time programs.
205
+
206
+ Use this dataclass to set heating time programs via set_heating_schedule().
207
+ Each day can have up to 3 time slots.
208
+
209
+ """
210
+
211
+
193
212
  @dataclass
194
213
  class DHWTimeSwitchPrograms:
195
214
  """Dataclass for DHW time switch programs."""
@@ -307,13 +326,13 @@ class EntityInfo(BaseModel, Generic[T]):
307
326
  unit: str
308
327
  desc: str
309
328
  value: T | None = None
310
- data_type: int = Field(alias="dataType", default=0)
329
+ data_type: int = Field(validation_alias="dataType", default=0)
311
330
  error: int = 0
312
331
  readonly: int = 0
313
332
  readwrite: int = 0
314
333
  precision: float | None = None
315
- data_type_name: str = Field(default="", alias="dataType_name")
316
- data_type_family: str = Field(default="", alias="dataType_family")
334
+ data_type_name: str = Field(default="", validation_alias="dataType_name")
335
+ data_type_family: str = Field(default="", validation_alias="dataType_family")
317
336
 
318
337
  @model_validator(mode="before")
319
338
  @classmethod
@@ -554,6 +573,29 @@ class HotWaterSchedule(BaseModel):
554
573
  dhw_time_program_standard_values: EntityInfo[int] | None = None
555
574
 
556
575
 
576
+ class HeatingTimeSwitchPrograms(BaseModel):
577
+ """Heating time switch programs for a specific heating circuit (READ).
578
+
579
+ The daily time programs (Monday-Sunday) use BSB-LAN dataType 9
580
+ (TIMEPROG) and return schedule strings like
581
+ ``"13:00-15:00 ##:##-##:## ##:##-##:##"`` where ``##:##`` marks
582
+ unused time slots.
583
+
584
+ ``standard_values`` is a YESNO enum (0=No, 1=Yes) that resets
585
+ all daily schedules back to the controller's factory defaults.
586
+
587
+ """
588
+
589
+ monday: EntityInfo[str | int] | None = None
590
+ tuesday: EntityInfo[str | int] | None = None
591
+ wednesday: EntityInfo[str | int] | None = None
592
+ thursday: EntityInfo[str | int] | None = None
593
+ friday: EntityInfo[str | int] | None = None
594
+ saturday: EntityInfo[str | int] | None = None
595
+ sunday: EntityInfo[str | int] | None = None
596
+ standard_values: EntityInfo[int] | None = None
597
+
598
+
557
599
  class DeviceTime(BaseModel):
558
600
  """Object holds device time information."""
559
601