python-bsblan 3.0.0__tar.gz → 3.1.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 (89) hide show
  1. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/codeql.yaml +3 -3
  2. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/labels.yaml +1 -1
  3. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/linting.yaml +12 -12
  4. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/release.yaml +3 -3
  5. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/stale.yaml +1 -1
  6. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/tests.yaml +5 -5
  7. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/typing.yaml +2 -2
  8. python_bsblan-3.1.1/.nvmrc +1 -0
  9. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/PKG-INFO +1 -1
  10. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/examples/control.py +9 -9
  11. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/pyproject.toml +24 -11
  12. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/bsblan.py +114 -29
  13. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/models.py +3 -3
  14. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/thermostat_hvac.json +1 -1
  15. python_bsblan-3.1.1/tests/test_api_initialization.py +103 -0
  16. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_api_validation.py +66 -1
  17. python_bsblan-3.1.1/tests/test_constants.py +152 -0
  18. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_hotwater_state.py +5 -8
  19. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_initialization.py +16 -4
  20. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_set_hotwater.py +10 -10
  21. python_bsblan-3.1.1/tests/test_temperature_unit.py +211 -0
  22. python_bsblan-3.1.1/tests/test_temperature_validation.py +166 -0
  23. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_thermostat.py +2 -2
  24. python_bsblan-3.1.1/uv.lock +2656 -0
  25. python_bsblan-3.0.0/.nvmrc +0 -1
  26. python_bsblan-3.0.0/tests/test_api_initialization.py +0 -50
  27. python_bsblan-3.0.0/tests/test_temperature_unit.py +0 -97
  28. python_bsblan-3.0.0/tests/test_temperature_validation.py +0 -64
  29. python_bsblan-3.0.0/uv.lock +0 -2316
  30. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.editorconfig +0 -0
  31. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.gitattributes +0 -0
  32. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/CODE_OF_CONDUCT.md +0 -0
  33. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/CONTRIBUTING.md +0 -0
  34. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  36. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  37. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/LICENSE.md +0 -0
  38. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/labels.yml +0 -0
  39. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/release-drafter.yml +0 -0
  40. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/renovate.json +0 -0
  41. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/lock.yaml +0 -0
  42. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/pr-labels.yaml +0 -0
  43. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/release-drafter.yaml +0 -0
  44. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.gitignore +0 -0
  45. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.pre-commit-config.yaml +0 -0
  46. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.prettierignore +0 -0
  47. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.yamllint +0 -0
  48. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/README.md +0 -0
  49. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/examples/ruff.toml +0 -0
  50. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/package-lock.json +0 -0
  51. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/package.json +0 -0
  52. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/sonar-project.properties +0 -0
  53. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/__init__.py +0 -0
  54. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/constants.py +1 -1
  55. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/exceptions.py +0 -0
  56. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/py.typed +0 -0
  57. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/utility.py +0 -0
  58. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/__init__.py +0 -0
  59. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/conftest.py +0 -0
  60. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/device.json +0 -0
  61. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/dict_version.json +0 -0
  62. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/hot_water_state.json +0 -0
  63. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/info.json +0 -0
  64. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/password.txt +0 -0
  65. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/sensor.json +0 -0
  66. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/state.json +0 -0
  67. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/static_state.json +0 -0
  68. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/thermostat_temp.json +0 -0
  69. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/time.json +0 -0
  70. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/ruff.toml +0 -0
  71. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_auth.py +0 -0
  72. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_bsblan.py +0 -0
  73. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_bsblan_edge_cases.py +0 -0
  74. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_configuration.py +0 -0
  75. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_context_manager.py +0 -0
  76. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_device.py +0 -0
  77. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_dhw_time_switch.py +0 -0
  78. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_entity_info.py +0 -0
  79. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_hot_water_additional.py +0 -0
  80. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_info.py +0 -0
  81. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_reset_validation.py +0 -0
  82. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_sensor.py +0 -0
  83. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_state.py +0 -0
  84. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_static_state.py +0 -0
  85. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_time.py +0 -0
  86. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility.py +0 -0
  87. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility_additional.py +0 -0
  88. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility_edge_cases.py +0 -0
  89. {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_version_errors.py +0 -0
@@ -17,8 +17,8 @@ jobs:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
19
  - name: ⤵️ Check out code from GitHub
20
- uses: actions/checkout@v4.2.2
20
+ uses: actions/checkout@v5.0.0
21
21
  - name: 🏗 Initialize CodeQL
22
- uses: github/codeql-action/init@v3.29.8
22
+ uses: github/codeql-action/init@v3.30.3
23
23
  - name: 🚀 Perform CodeQL Analysis
24
- uses: github/codeql-action/analyze@v3.29.8
24
+ uses: github/codeql-action/analyze@v3.30.3
@@ -16,7 +16,7 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - name: ⤵️ Check out code from GitHub
19
- uses: actions/checkout@v4.2.2
19
+ uses: actions/checkout@v5.0.0
20
20
  - name: 🚀 Run Label Syncer
21
21
  uses: micnncim/action-label-syncer@v1.3.0
22
22
  env:
@@ -16,14 +16,14 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - name: ⤵️ Check out code from GitHub
19
- uses: actions/checkout@v4.2.2
19
+ uses: actions/checkout@v5.0.0
20
20
  - name: 🏗 Set up uv
21
21
  uses: astral-sh/setup-uv@v6
22
22
  with:
23
23
  enable-cache: true
24
24
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
25
25
  id: python
26
- uses: actions/setup-python@v5.6.0
26
+ uses: actions/setup-python@v6.0.0
27
27
  with:
28
28
  python-version: ${{ env.DEFAULT_PYTHON }}
29
29
  - name: 🏗 Install Python dependencies
@@ -36,14 +36,14 @@ jobs:
36
36
  runs-on: ubuntu-latest
37
37
  steps:
38
38
  - name: ⤵️ Check out code from GitHub
39
- uses: actions/checkout@v4.2.2
39
+ uses: actions/checkout@v5.0.0
40
40
  - name: 🏗 Set up uv
41
41
  uses: astral-sh/setup-uv@v6
42
42
  with:
43
43
  enable-cache: true
44
44
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
45
45
  id: python
46
- uses: actions/setup-python@v5.6.0
46
+ uses: actions/setup-python@v6.0.0
47
47
  with:
48
48
  python-version: ${{ env.DEFAULT_PYTHON }}
49
49
  - name: 🏗 Install Python dependencies
@@ -58,14 +58,14 @@ jobs:
58
58
  runs-on: ubuntu-latest
59
59
  steps:
60
60
  - name: ⤵️ Check out code from GitHub
61
- uses: actions/checkout@v4.2.2
61
+ uses: actions/checkout@v5.0.0
62
62
  - name: 🏗 Set up uv
63
63
  uses: astral-sh/setup-uv@v6
64
64
  with:
65
65
  enable-cache: true
66
66
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
67
67
  id: python
68
- uses: actions/setup-python@v5.6.0
68
+ uses: actions/setup-python@v6.0.0
69
69
  with:
70
70
  python-version: ${{ env.DEFAULT_PYTHON }}
71
71
  - name: 🏗 Install Python dependencies
@@ -102,14 +102,14 @@ jobs:
102
102
  runs-on: ubuntu-latest
103
103
  steps:
104
104
  - name: ⤵️ Check out code from GitHub
105
- uses: actions/checkout@v4.2.2
105
+ uses: actions/checkout@v5.0.0
106
106
  - name: 🏗 Set up uv
107
107
  uses: astral-sh/setup-uv@v6
108
108
  with:
109
109
  enable-cache: true
110
110
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
111
111
  id: python
112
- uses: actions/setup-python@v5.6.0
112
+ uses: actions/setup-python@v6.0.0
113
113
  with:
114
114
  python-version: ${{ env.DEFAULT_PYTHON }}
115
115
  - name: 🏗 Install Python dependencies
@@ -122,14 +122,14 @@ jobs:
122
122
  runs-on: ubuntu-latest
123
123
  steps:
124
124
  - name: ⤵️ Check out code from GitHub
125
- uses: actions/checkout@v4.2.2
125
+ uses: actions/checkout@v5.0.0
126
126
  - name: 🏗 Set up uv
127
127
  uses: astral-sh/setup-uv@v6
128
128
  with:
129
129
  enable-cache: true
130
130
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
131
131
  id: python
132
- uses: actions/setup-python@v5.6.0
132
+ uses: actions/setup-python@v6.0.0
133
133
  with:
134
134
  python-version: ${{ env.DEFAULT_PYTHON }}
135
135
  - name: 🏗 Install Python dependencies
@@ -142,14 +142,14 @@ jobs:
142
142
  runs-on: ubuntu-latest
143
143
  steps:
144
144
  - name: ⤵️ Check out code from GitHub
145
- uses: actions/checkout@v4.2.2
145
+ uses: actions/checkout@v5.0.0
146
146
  - name: 🏗 Set up uv
147
147
  uses: astral-sh/setup-uv@v6
148
148
  with:
149
149
  enable-cache: true
150
150
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
151
151
  id: python
152
- uses: actions/setup-python@v5.6.0
152
+ uses: actions/setup-python@v6.0.0
153
153
  with:
154
154
  python-version: ${{ env.DEFAULT_PYTHON }}
155
155
  - name: 🏗 Install Python dependencies
@@ -22,14 +22,14 @@ jobs:
22
22
  id-token: write
23
23
  steps:
24
24
  - name: ⤵️ Check out code from GitHub
25
- uses: actions/checkout@v4.2.2
25
+ uses: actions/checkout@v5.0.0
26
26
  - name: 🏗 Set up uv
27
27
  uses: astral-sh/setup-uv@v6
28
28
  with:
29
29
  enable-cache: true
30
30
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
31
31
  id: python
32
- uses: actions/setup-python@v5.6.0
32
+ uses: actions/setup-python@v6.0.0
33
33
  with:
34
34
  python-version: ${{ env.DEFAULT_PYTHON }}
35
35
  - name: 🏗 Install dependencies
@@ -43,7 +43,7 @@ jobs:
43
43
  - name: 🏗 Build package
44
44
  run: uv build
45
45
  - name: 🚀 Publish to PyPi
46
- uses: pypa/gh-action-pypi-publish@v1.12.4
46
+ uses: pypa/gh-action-pypi-publish@v1.13.0
47
47
  with:
48
48
  verbose: true
49
49
  print-hash: true
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - name: 🚀 Run stale
16
- uses: actions/stale@v9.1.0
16
+ uses: actions/stale@v10.1.0
17
17
  with:
18
18
  repo-token: ${{ secrets.GITHUB_TOKEN }}
19
19
  days-before-stale: 30
@@ -19,14 +19,14 @@ jobs:
19
19
  python: ["3.11", "3.12"]
20
20
  steps:
21
21
  - name: ⤵️ Check out code from GitHub
22
- uses: actions/checkout@v4.2.2
22
+ uses: actions/checkout@v5.0.0
23
23
  - name: 🏗 Set up uv
24
24
  uses: astral-sh/setup-uv@v6
25
25
  with:
26
26
  enable-cache: true
27
27
  - name: 🏗 Set up Python ${{ matrix.python }}
28
28
  id: python
29
- uses: actions/setup-python@v5.6.0
29
+ uses: actions/setup-python@v6.0.0
30
30
  with:
31
31
  python-version: ${{ matrix.python }}
32
32
  - name: 🏗 Install dependencies
@@ -45,7 +45,7 @@ jobs:
45
45
  needs: pytest
46
46
  steps:
47
47
  - name: ⤵️ Check out code from GitHub
48
- uses: actions/checkout@v4.2.2
48
+ uses: actions/checkout@v5.0.0
49
49
  with:
50
50
  fetch-depth: 0
51
51
  - name: ⬇️ Download coverage data
@@ -56,7 +56,7 @@ jobs:
56
56
  enable-cache: true
57
57
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
58
58
  id: python
59
- uses: actions/setup-python@v5.6.0
59
+ uses: actions/setup-python@v6.0.0
60
60
  with:
61
61
  python-version: ${{ env.DEFAULT_PYTHON }}
62
62
  - name: 🏗 Install dependencies
@@ -66,7 +66,7 @@ jobs:
66
66
  uv run coverage combine coverage*/.coverage*
67
67
  uv run coverage xml -i
68
68
  - name: 🚀 Upload coverage report
69
- uses: codecov/codecov-action@v5.4.3
69
+ uses: codecov/codecov-action@v5.5.1
70
70
  with:
71
71
  token: ${{ secrets.CODECOV_TOKEN }}
72
72
  - name: SonarCloud Scan
@@ -16,14 +16,14 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
18
  - name: ⤵️ Check out code from GitHub
19
- uses: actions/checkout@v4.2.2
19
+ uses: actions/checkout@v5.0.0
20
20
  - name: 🏗 Set up uv
21
21
  uses: astral-sh/setup-uv@v6
22
22
  with:
23
23
  enable-cache: true
24
24
  - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
25
25
  id: python
26
- uses: actions/setup-python@v5.6.0
26
+ uses: actions/setup-python@v6.0.0
27
27
  with:
28
28
  python-version: ${{ env.DEFAULT_PYTHON }}
29
29
  - name: 🏗 Install dependencies
@@ -0,0 +1 @@
1
+ 22.19.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-bsblan
3
- Version: 3.0.0
3
+ Version: 3.1.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
@@ -2,8 +2,8 @@
2
2
  """Asynchronous Python client for BSBLan.
3
3
 
4
4
  This example demonstrates the optimized hot water functionality:
5
- - HotWaterState: Essential parameters for frequent polling (6 fields)
6
- - HotWaterConfig: Configuration parameters checked less frequently (15 fields)
5
+ - HotWaterState: Essential parameters for frequent polling (5 fields)
6
+ - HotWaterConfig: Configuration parameters checked less frequently (16 fields)
7
7
  - HotWaterSchedule: Time program schedules checked occasionally (8 fields)
8
8
 
9
9
  This three-tier approach reduces API calls by 79% for regular monitoring.
@@ -169,9 +169,6 @@ async def print_hot_water_state(hot_water_state: HotWaterState) -> None:
169
169
  "Nominal Setpoint": await get_attribute(
170
170
  hot_water_state.nominal_setpoint, "value", "N/A"
171
171
  ),
172
- "Reduced Setpoint": await get_attribute(
173
- hot_water_state.reduced_setpoint, "value", "N/A"
174
- ),
175
172
  "Release": await get_attribute(hot_water_state.release, "desc", "N/A"),
176
173
  "Current Temperature": await get_attribute(
177
174
  hot_water_state.dhw_actual_value_top_temperature, "value", "N/A"
@@ -195,6 +192,9 @@ async def print_hot_water_config(hot_water_config: HotWaterConfig) -> None:
195
192
  "Nominal Setpoint Max": await get_attribute(
196
193
  hot_water_config.nominal_setpoint_max, "value", "N/A"
197
194
  ),
195
+ "Reduced Setpoint": await get_attribute(
196
+ hot_water_config.reduced_setpoint, "value", "N/A"
197
+ ),
198
198
  "Legionella Function": await get_attribute(
199
199
  hot_water_config.legionella_function, "desc", "N/A"
200
200
  ),
@@ -300,14 +300,14 @@ async def main() -> None:
300
300
  try:
301
301
  hot_water_config: HotWaterConfig = await bsblan.hot_water_config()
302
302
  await print_hot_water_config(hot_water_config)
303
- except Exception as e: # noqa: BLE001
303
+ except Exception as e: # noqa: BLE001 - Broad exception for demo purposes
304
304
  print(f"Hot water configuration not available: {e}")
305
305
 
306
306
  # Get hot water schedule (time programs)
307
307
  try:
308
308
  hot_water_schedule: HotWaterSchedule = await bsblan.hot_water_schedule()
309
309
  await print_hot_water_schedule(hot_water_schedule)
310
- except Exception as e: # noqa: BLE001
310
+ except Exception as e: # noqa: BLE001 - Broad exception for demo purposes
311
311
  print(f"Hot water schedule not available: {e}")
312
312
 
313
313
  # Example: Set DHW time program for Monday
@@ -321,8 +321,8 @@ async def main() -> None:
321
321
  # Example: Set device time
322
322
  print("\nSetting device time to current system time")
323
323
  # Get current local system time and format it for BSB-LAN (DD.MM.YYYY HH:MM:SS)
324
- # Note: Using local time intentionally to sync BSB-LAN with system clock
325
- current_time = datetime.now().replace(microsecond=0) # noqa: DTZ005
324
+ # Note: Using local time intentionally for this demo to sync BSB-LAN
325
+ current_time = datetime.now().replace(microsecond=0) # noqa: DTZ005 - Demo uses local time
326
326
  formatted_time = current_time.strftime("%d.%m.%Y %H:%M:%S")
327
327
  print(f"Current system time: {formatted_time}")
328
328
  await bsblan.set_time(formatted_time)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-bsblan"
3
- version = "3.0.0"
3
+ version = "3.1.1"
4
4
  description = "Asynchronous Python client for BSBLAN API"
5
5
  authors = [
6
6
  {name = "Willem-Jan van Rootselaar", email = "liudgervr@gmail.com"}
@@ -46,11 +46,24 @@ packages = ["src/bsblan"]
46
46
 
47
47
  [tool.coverage.run]
48
48
  plugins = ["covdefaults"]
49
- source = ["bsblan"]
49
+ source = ["src/bsblan"]
50
+ omit = [
51
+ "tests/*",
52
+ "*/tests/*",
53
+ "test_*",
54
+ "*test*"
55
+ ]
50
56
 
51
57
  [tool.coverage.report]
52
58
  show_missing = true
53
59
  fail_under = 53
60
+ exclude_lines = [
61
+ "pragma: no cover",
62
+ "def __repr__",
63
+ "raise AssertionError",
64
+ "raise NotImplementedError",
65
+ "if __name__ == .__main__.:",
66
+ ]
54
67
 
55
68
  [tool.mypy]
56
69
  # Specify the target platform details in config, so your developers are
@@ -164,27 +177,27 @@ build-backend = "hatchling.build"
164
177
  dev = [
165
178
  "aresponses==3.0.0",
166
179
  "bandit==1.8.6",
167
- "black==25.1.0",
168
- "blacken-docs==1.19.1",
180
+ "black==25.11.0",
181
+ "blacken-docs==1.20.0",
169
182
  "codespell==2.4.1",
170
183
  "covdefaults==2.3.0",
171
- "coverage==7.10.3",
184
+ "coverage==7.10.6",
172
185
  "darglint==1.8.1",
173
186
  "flake8==7.3.0",
174
187
  "flake8-simplify==0.22.0",
175
188
  # hatch is required to support type hinting and proper packaging of the py.typed file.
176
189
  "hatch>=1.14.1",
177
190
  "isort==6.0.1",
178
- "mypy==1.17.1",
191
+ "mypy==1.18.2",
179
192
  "pre-commit==4.3.0",
180
193
  "pre-commit-hooks==6.0.0",
181
- "pylint==3.3.8",
194
+ "pylint==3.3.9",
182
195
  "pytest>=8.3.5",
183
- "pytest-asyncio==1.1.0",
184
- "pytest-cov==6.2.1",
196
+ "pytest-asyncio==1.3.0",
197
+ "pytest-cov==7.0.0",
185
198
  "pyupgrade==3.20.0",
186
- "ruff==0.12.8",
187
- "safety==3.6.0",
199
+ "ruff==0.14.4",
200
+ "safety==3.6.1",
188
201
  "vulture==2.14",
189
202
  "yamllint==1.37.1",
190
203
  ]
@@ -138,7 +138,15 @@ class BSBLAN:
138
138
 
139
139
  # Initialize API data if not already done
140
140
  if self._api_data is None:
141
- self._api_data = API_VERSIONS[self._api_version]
141
+ # Copy each section dictionary to avoid modifying the shared constant
142
+ source_config: APIConfig = API_VERSIONS[self._api_version]
143
+ self._api_data = cast(
144
+ "APIConfig",
145
+ {
146
+ section: cast("dict[str, str]", params).copy()
147
+ for section, params in source_config.items()
148
+ },
149
+ )
142
150
 
143
151
  # Initialize the API validator
144
152
  self._api_validator = APIValidator(self._api_data)
@@ -152,14 +160,25 @@ class BSBLAN:
152
160
  "hot_water",
153
161
  ]
154
162
  for section in sections:
155
- await self._validate_api_section(section)
163
+ response_data = await self._validate_api_section(section)
156
164
 
157
- async def _validate_api_section(self, section: SectionLiteral) -> None:
165
+ # Extract temperature unit from heating section validation
166
+ # (parameter 710 - target_temperature is always in heating section)
167
+ if section == "heating" and response_data:
168
+ self._extract_temperature_unit_from_response(response_data)
169
+
170
+ async def _validate_api_section(
171
+ self, section: SectionLiteral
172
+ ) -> dict[str, Any] | None:
158
173
  """Validate a specific section of the API configuration.
159
174
 
160
175
  Args:
161
176
  section: The section name to validate
162
177
 
178
+ Returns:
179
+ dict[str, Any] | None: The response data from the device, or None if
180
+ section was already validated or validation failed
181
+
163
182
  Raises:
164
183
  BSBLANError: If the API validator is not initialized
165
184
 
@@ -174,7 +193,7 @@ class BSBLAN:
174
193
  api_validator = self._api_validator
175
194
 
176
195
  if api_validator.is_section_validated(section):
177
- return
196
+ return None
178
197
 
179
198
  # Get parameters for the section
180
199
  try:
@@ -204,6 +223,8 @@ class BSBLAN:
204
223
  # Reset validation state for this section
205
224
  api_validator.reset_validation(section)
206
225
  raise
226
+ else:
227
+ return response_data
207
228
 
208
229
  def _populate_hot_water_cache(self) -> None:
209
230
  """Populate the hot water parameter cache with all available parameters."""
@@ -215,6 +236,43 @@ class BSBLAN:
215
236
  self._hot_water_param_cache = hotwater_params.copy()
216
237
  logger.debug("Cached %d hot water parameters", len(self._hot_water_param_cache))
217
238
 
239
+ def _extract_temperature_unit_from_response(
240
+ self, response_data: dict[str, Any]
241
+ ) -> None:
242
+ """Extract temperature unit from heating section response data.
243
+
244
+ Gets the unit from parameter 710 (target_temperature) which is always
245
+ present in the heating section.
246
+
247
+ Args:
248
+ response_data: The response data from heating section validation
249
+
250
+ """
251
+ # Look for parameter 710 (target_temperature) in the response
252
+ for param_id, param_data in response_data.items():
253
+ # Check if this is parameter 710 and has unit information
254
+ if param_id == "710" and isinstance(param_data, dict):
255
+ unit = param_data.get("unit", "")
256
+ if unit in ("°C", "°C"):
257
+ self._temperature_unit = "°C"
258
+ elif unit == "°F":
259
+ self._temperature_unit = "°F"
260
+ else:
261
+ # Keep default if unit is empty or unknown
262
+ logger.debug(
263
+ "Unknown or empty temperature unit from parameter 710: '%s'. "
264
+ "Using default (°C)",
265
+ unit,
266
+ )
267
+ logger.debug("Temperature unit set to: %s", self._temperature_unit)
268
+ return
269
+
270
+ # If we didn't find parameter 710, log a warning
271
+ logger.warning(
272
+ "Could not find parameter 710 in heating section response. "
273
+ "Using default temperature unit (°C)"
274
+ )
275
+
218
276
  def set_hot_water_cache(self, params: dict[str, str]) -> None:
219
277
  """Set the hot water parameter cache manually (for testing).
220
278
 
@@ -256,23 +314,40 @@ class BSBLAN:
256
314
  raise BSBLANVersionError(VERSION_ERROR_MSG)
257
315
 
258
316
  async def _initialize_temperature_range(self) -> None:
259
- """Initialize the temperature range from static values."""
317
+ """Initialize the temperature range from static values.
318
+
319
+ Note: Temperature unit is extracted during API validator initialization
320
+ from the heating section response (parameter 710), so no extra API call
321
+ is needed here.
322
+ """
260
323
  if not self._temperature_range_initialized:
261
- static_values = await self.static_values()
262
- self._min_temp = float(static_values.min_temp.value)
263
- self._max_temp = float(static_values.max_temp.value)
324
+ # Try to get temperature range from static values
325
+ try:
326
+ static_values = await self.static_values()
327
+ if static_values.min_temp is not None:
328
+ self._min_temp = float(static_values.min_temp.value)
329
+ logger.debug("Min temperature initialized: %f", self._min_temp)
330
+ else:
331
+ logger.warning(
332
+ "min_temp not available from device, "
333
+ "temperature range will be None"
334
+ )
335
+
336
+ if static_values.max_temp is not None:
337
+ self._max_temp = float(static_values.max_temp.value)
338
+ logger.debug("Max temperature initialized: %f", self._max_temp)
339
+ else:
340
+ logger.warning(
341
+ "max_temp not available from device, "
342
+ "temperature range will be None"
343
+ )
344
+ except BSBLANError as err:
345
+ logger.warning(
346
+ "Failed to get static values: %s. Temperature range will be None",
347
+ str(err),
348
+ )
349
+
264
350
  self._temperature_range_initialized = True
265
- logger.debug(
266
- "Temperature range initialized: min=%f, max=%f",
267
- self._min_temp,
268
- self._max_temp,
269
- )
270
- # also set unit of temperature
271
- if static_values.min_temp.unit in ("°C", "°C"):
272
- self._temperature_unit = "°C"
273
- else:
274
- self._temperature_unit = "°F"
275
- logger.debug("Temperature unit: %s", self._temperature_unit)
276
351
 
277
352
  @property
278
353
  def get_temperature_unit(self) -> str:
@@ -301,7 +376,15 @@ class BSBLAN:
301
376
  if self._api_data is None:
302
377
  if self._api_version is None:
303
378
  raise BSBLANError(API_VERSION_ERROR_MSG)
304
- self._api_data = API_VERSIONS[self._api_version]
379
+ # Copy each section dictionary to avoid modifying the shared constant
380
+ source_config: APIConfig = API_VERSIONS[self._api_version]
381
+ self._api_data = cast(
382
+ "APIConfig",
383
+ {
384
+ section: cast("dict[str, str]", params).copy()
385
+ for section, params in source_config.items()
386
+ },
387
+ )
305
388
  logger.debug("API data initialized for version: %s", self._api_version)
306
389
  if self._api_data is None:
307
390
  raise BSBLANError(API_DATA_NOT_INITIALIZED_ERROR_MSG)
@@ -358,8 +441,10 @@ class BSBLAN:
358
441
  raise BSBLANConnectionError(BSBLANConnectionError.message_error) from e
359
442
  except aiohttp.ClientError as e:
360
443
  raise BSBLANConnectionError(BSBLANConnectionError.message_error) from e
361
- except ValueError as e:
362
- raise BSBLANError(str(e)) from e
444
+ except (ValueError, UnicodeDecodeError) as e:
445
+ # Handle JSON decode errors and other parsing issues
446
+ error_msg = f"Invalid response format from BSB-LAN device: {e!s}"
447
+ raise BSBLANError(error_msg) from e
363
448
 
364
449
  def _process_response(
365
450
  self, response_data: dict[str, Any], base_path: str
@@ -610,7 +695,7 @@ class BSBLAN:
610
695
  state.update(
611
696
  {
612
697
  "Parameter": "700",
613
- "EnumValue": HVAC_MODE_DICT_REVERSE[hvac_mode],
698
+ "Value": str(HVAC_MODE_DICT_REVERSE[hvac_mode]),
614
699
  "Type": "1",
615
700
  },
616
701
  )
@@ -946,7 +1031,7 @@ class BSBLAN:
946
1031
  state.update(
947
1032
  {
948
1033
  "Parameter": "1600",
949
- "EnumValue": operating_mode,
1034
+ "Value": str(operating_mode),
950
1035
  "Type": "1",
951
1036
  },
952
1037
  )
@@ -954,7 +1039,7 @@ class BSBLAN:
954
1039
  state.update(
955
1040
  {
956
1041
  "Parameter": "1601",
957
- "EnumValue": eco_mode_selection,
1042
+ "Value": str(eco_mode_selection),
958
1043
  "Type": "1",
959
1044
  },
960
1045
  )
@@ -962,7 +1047,7 @@ class BSBLAN:
962
1047
  state.update(
963
1048
  {
964
1049
  "Parameter": "1630",
965
- "EnumValue": dhw_charging_priority,
1050
+ "Value": str(dhw_charging_priority),
966
1051
  "Type": "1",
967
1052
  },
968
1053
  )
@@ -978,7 +1063,7 @@ class BSBLAN:
978
1063
  state.update(
979
1064
  {
980
1065
  "Parameter": "1647",
981
- "EnumValue": legionella_circulation_pump,
1066
+ "Value": str(legionella_circulation_pump),
982
1067
  "Type": "1",
983
1068
  },
984
1069
  )
@@ -994,7 +1079,7 @@ class BSBLAN:
994
1079
  state.update(
995
1080
  {
996
1081
  "Parameter": "1660",
997
- "EnumValue": dhw_circulation_pump_release,
1082
+ "Value": str(dhw_circulation_pump_release),
998
1083
  "Type": "1",
999
1084
  },
1000
1085
  )
@@ -1018,7 +1103,7 @@ class BSBLAN:
1018
1103
  state.update(
1019
1104
  {
1020
1105
  "Parameter": "1680",
1021
- "EnumValue": operating_mode_changeover,
1106
+ "Value": str(operating_mode_changeover),
1022
1107
  "Type": "1",
1023
1108
  },
1024
1109
  )
@@ -164,8 +164,8 @@ class State(DataClassJSONMixin):
164
164
  class StaticState(DataClassJSONMixin):
165
165
  """Class for entities that are not changing."""
166
166
 
167
- min_temp: EntityInfo
168
- max_temp: EntityInfo
167
+ min_temp: EntityInfo | None = None
168
+ max_temp: EntityInfo | None = None
169
169
 
170
170
 
171
171
  @dataclass
@@ -186,7 +186,6 @@ class HotWaterState(DataClassJSONMixin):
186
186
 
187
187
  operating_mode: EntityInfo | None = None
188
188
  nominal_setpoint: EntityInfo | None = None
189
- reduced_setpoint: EntityInfo | None = None
190
189
  release: EntityInfo | None = None
191
190
  dhw_actual_value_top_temperature: EntityInfo | None = None
192
191
  state_dhw_pump: EntityInfo | None = None
@@ -202,6 +201,7 @@ class HotWaterConfig(DataClassJSONMixin): # pylint: disable=too-many-instance-a
202
201
 
203
202
  eco_mode_selection: EntityInfo | None = None
204
203
  nominal_setpoint_max: EntityInfo | None = None
204
+ reduced_setpoint: EntityInfo | None = None
205
205
  dhw_charging_priority: EntityInfo | None = None
206
206
  operating_mode_changeover: EntityInfo | None = None
207
207
  # Legionella protection settings
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "Parameter": "700",
3
- "EnumValue": "3",
3
+ "Value": "3",
4
4
  "Type": "1"
5
5
  }