python-bsblan 3.0.0__tar.gz → 3.1.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 (85) hide show
  1. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/codeql.yaml +3 -3
  2. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/labels.yaml +1 -1
  3. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/linting.yaml +6 -6
  4. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/release.yaml +1 -1
  5. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/tests.yaml +3 -3
  6. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/typing.yaml +1 -1
  7. python_bsblan-3.1.0/.nvmrc +1 -0
  8. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/PKG-INFO +1 -1
  9. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/examples/control.py +9 -9
  10. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/pyproject.toml +21 -8
  11. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/bsblan.py +11 -9
  12. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/thermostat_hvac.json +1 -1
  13. python_bsblan-3.1.0/tests/test_constants.py +152 -0
  14. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_hotwater_state.py +5 -8
  15. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_set_hotwater.py +10 -10
  16. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_thermostat.py +2 -2
  17. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/uv.lock +508 -482
  18. python_bsblan-3.0.0/.nvmrc +0 -1
  19. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.editorconfig +0 -0
  20. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.gitattributes +0 -0
  21. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/CODE_OF_CONDUCT.md +0 -0
  22. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/CONTRIBUTING.md +0 -0
  23. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/LICENSE.md +0 -0
  27. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/labels.yml +0 -0
  28. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/release-drafter.yml +0 -0
  29. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/renovate.json +0 -0
  30. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/lock.yaml +0 -0
  31. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/pr-labels.yaml +0 -0
  32. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/release-drafter.yaml +0 -0
  33. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.github/workflows/stale.yaml +0 -0
  34. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.gitignore +0 -0
  35. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.pre-commit-config.yaml +0 -0
  36. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.prettierignore +0 -0
  37. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/.yamllint +0 -0
  38. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/README.md +0 -0
  39. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/examples/ruff.toml +0 -0
  40. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/package-lock.json +0 -0
  41. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/package.json +0 -0
  42. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/sonar-project.properties +0 -0
  43. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/__init__.py +0 -0
  44. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/constants.py +1 -1
  45. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/exceptions.py +0 -0
  46. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/models.py +1 -1
  47. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/py.typed +0 -0
  48. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/src/bsblan/utility.py +0 -0
  49. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/__init__.py +0 -0
  50. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/conftest.py +0 -0
  51. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/device.json +0 -0
  52. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/dict_version.json +0 -0
  53. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/hot_water_state.json +0 -0
  54. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/info.json +0 -0
  55. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/password.txt +0 -0
  56. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/sensor.json +0 -0
  57. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/state.json +0 -0
  58. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/static_state.json +0 -0
  59. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/thermostat_temp.json +0 -0
  60. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/fixtures/time.json +0 -0
  61. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/ruff.toml +0 -0
  62. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_api_initialization.py +0 -0
  63. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_api_validation.py +0 -0
  64. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_auth.py +0 -0
  65. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_bsblan.py +0 -0
  66. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_bsblan_edge_cases.py +0 -0
  67. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_configuration.py +0 -0
  68. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_context_manager.py +0 -0
  69. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_device.py +0 -0
  70. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_dhw_time_switch.py +0 -0
  71. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_entity_info.py +0 -0
  72. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_hot_water_additional.py +0 -0
  73. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_info.py +0 -0
  74. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_initialization.py +0 -0
  75. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_reset_validation.py +0 -0
  76. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_sensor.py +0 -0
  77. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_state.py +0 -0
  78. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_static_state.py +0 -0
  79. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_temperature_unit.py +0 -0
  80. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_temperature_validation.py +0 -0
  81. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_time.py +0 -0
  82. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_utility.py +0 -0
  83. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_utility_additional.py +0 -0
  84. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/tests/test_utility_edge_cases.py +0 -0
  85. {python_bsblan-3.0.0 → python_bsblan-3.1.0}/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,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: 🏗 Set up uv
21
21
  uses: astral-sh/setup-uv@v6
22
22
  with:
@@ -36,7 +36,7 @@ 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:
@@ -58,7 +58,7 @@ 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:
@@ -102,7 +102,7 @@ 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:
@@ -122,7 +122,7 @@ 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:
@@ -142,7 +142,7 @@ 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:
@@ -22,7 +22,7 @@ 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:
@@ -19,7 +19,7 @@ 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:
@@ -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
@@ -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,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: 🏗 Set up uv
21
21
  uses: astral-sh/setup-uv@v6
22
22
  with:
@@ -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.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
@@ -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.0"
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
@@ -165,26 +178,26 @@ dev = [
165
178
  "aresponses==3.0.0",
166
179
  "bandit==1.8.6",
167
180
  "black==25.1.0",
168
- "blacken-docs==1.19.1",
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.1",
179
192
  "pre-commit==4.3.0",
180
193
  "pre-commit-hooks==6.0.0",
181
194
  "pylint==3.3.8",
182
195
  "pytest>=8.3.5",
183
196
  "pytest-asyncio==1.1.0",
184
- "pytest-cov==6.2.1",
197
+ "pytest-cov==6.3.0",
185
198
  "pyupgrade==3.20.0",
186
- "ruff==0.12.8",
187
- "safety==3.6.0",
199
+ "ruff==0.13.0",
200
+ "safety==3.6.1",
188
201
  "vulture==2.14",
189
202
  "yamllint==1.37.1",
190
203
  ]
@@ -358,8 +358,10 @@ class BSBLAN:
358
358
  raise BSBLANConnectionError(BSBLANConnectionError.message_error) from e
359
359
  except aiohttp.ClientError as e:
360
360
  raise BSBLANConnectionError(BSBLANConnectionError.message_error) from e
361
- except ValueError as e:
362
- raise BSBLANError(str(e)) from e
361
+ except (ValueError, UnicodeDecodeError) as e:
362
+ # Handle JSON decode errors and other parsing issues
363
+ error_msg = f"Invalid response format from BSB-LAN device: {e!s}"
364
+ raise BSBLANError(error_msg) from e
363
365
 
364
366
  def _process_response(
365
367
  self, response_data: dict[str, Any], base_path: str
@@ -610,7 +612,7 @@ class BSBLAN:
610
612
  state.update(
611
613
  {
612
614
  "Parameter": "700",
613
- "EnumValue": HVAC_MODE_DICT_REVERSE[hvac_mode],
615
+ "Value": str(HVAC_MODE_DICT_REVERSE[hvac_mode]),
614
616
  "Type": "1",
615
617
  },
616
618
  )
@@ -946,7 +948,7 @@ class BSBLAN:
946
948
  state.update(
947
949
  {
948
950
  "Parameter": "1600",
949
- "EnumValue": operating_mode,
951
+ "Value": str(operating_mode),
950
952
  "Type": "1",
951
953
  },
952
954
  )
@@ -954,7 +956,7 @@ class BSBLAN:
954
956
  state.update(
955
957
  {
956
958
  "Parameter": "1601",
957
- "EnumValue": eco_mode_selection,
959
+ "Value": str(eco_mode_selection),
958
960
  "Type": "1",
959
961
  },
960
962
  )
@@ -962,7 +964,7 @@ class BSBLAN:
962
964
  state.update(
963
965
  {
964
966
  "Parameter": "1630",
965
- "EnumValue": dhw_charging_priority,
967
+ "Value": str(dhw_charging_priority),
966
968
  "Type": "1",
967
969
  },
968
970
  )
@@ -978,7 +980,7 @@ class BSBLAN:
978
980
  state.update(
979
981
  {
980
982
  "Parameter": "1647",
981
- "EnumValue": legionella_circulation_pump,
983
+ "Value": str(legionella_circulation_pump),
982
984
  "Type": "1",
983
985
  },
984
986
  )
@@ -994,7 +996,7 @@ class BSBLAN:
994
996
  state.update(
995
997
  {
996
998
  "Parameter": "1660",
997
- "EnumValue": dhw_circulation_pump_release,
999
+ "Value": str(dhw_circulation_pump_release),
998
1000
  "Type": "1",
999
1001
  },
1000
1002
  )
@@ -1018,7 +1020,7 @@ class BSBLAN:
1018
1020
  state.update(
1019
1021
  {
1020
1022
  "Parameter": "1680",
1021
- "EnumValue": operating_mode_changeover,
1023
+ "Value": str(operating_mode_changeover),
1022
1024
  "Type": "1",
1023
1025
  },
1024
1026
  )
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "Parameter": "700",
3
- "EnumValue": "3",
3
+ "Value": "3",
4
4
  "Type": "1"
5
5
  }
@@ -0,0 +1,152 @@
1
+ """Test BSBLAN constants module."""
2
+
3
+ import pytest
4
+
5
+ from bsblan.constants import (
6
+ API_V1,
7
+ API_V3,
8
+ BASE_HOT_WATER_PARAMS,
9
+ HOT_WATER_CONFIG_PARAMS,
10
+ HOT_WATER_ESSENTIAL_PARAMS,
11
+ HOT_WATER_SCHEDULE_PARAMS,
12
+ build_api_config,
13
+ )
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ ("version", "expected_includes", "expected_excludes"),
18
+ [
19
+ (
20
+ "v1",
21
+ {"700", "710", "714", "730"}, # hvac_mode, target_temp, min_temp, v1_max
22
+ {"770", "716"}, # v3_boost, v3_max_temp
23
+ ),
24
+ (
25
+ "v3",
26
+ {"700", "710", "714", "770", "716"}, # base + v3 extensions
27
+ {"730"}, # v1_max_temp
28
+ ),
29
+ (
30
+ "v5", # Unknown version
31
+ {"700", "710", "714"}, # only base parameters
32
+ {"770", "730", "716"}, # no extensions
33
+ ),
34
+ ],
35
+ )
36
+ def test_build_api_config_versions(
37
+ version: str,
38
+ expected_includes: set[str],
39
+ expected_excludes: set[str],
40
+ ) -> None:
41
+ """Test building API config for different versions."""
42
+ config = build_api_config(version)
43
+
44
+ # Check expected parameters are included
45
+ for param_id in expected_includes:
46
+ assert param_id in (
47
+ config["heating"]
48
+ | config["staticValues"]
49
+ | config["device"]
50
+ | config["sensor"]
51
+ | config["hot_water"]
52
+ ), f"Parameter {param_id} missing in {version} config"
53
+
54
+ # Check excluded parameters are not included
55
+ for param_id in expected_excludes:
56
+ assert param_id not in (config["heating"] | config["staticValues"]), (
57
+ f"Parameter {param_id} should not be in {version} config"
58
+ )
59
+
60
+
61
+ @pytest.mark.parametrize(
62
+ ("api_config", "should_have", "should_not_have"),
63
+ [
64
+ (API_V1, {"730"}, {"770", "716"}), # V1 has 730, not 770/716
65
+ (API_V3, {"770", "716"}, {"730"}), # V3 has 770/716, not 730
66
+ ],
67
+ )
68
+ def test_pre_built_api_configurations(
69
+ api_config: dict[str, dict[str, str]],
70
+ should_have: set[str],
71
+ should_not_have: set[str],
72
+ ) -> None:
73
+ """Test that pre-built API configurations are correct."""
74
+ all_params = set(api_config["heating"].keys()) | set(
75
+ api_config["staticValues"].keys()
76
+ )
77
+
78
+ for param_id in should_have:
79
+ assert param_id in all_params
80
+
81
+ for param_id in should_not_have:
82
+ assert param_id not in all_params
83
+
84
+
85
+ def test_hot_water_parameter_groups_completeness() -> None:
86
+ """Test that hot water parameter groups cover all parameters."""
87
+ all_grouped_params = (
88
+ HOT_WATER_ESSENTIAL_PARAMS | HOT_WATER_CONFIG_PARAMS | HOT_WATER_SCHEDULE_PARAMS
89
+ )
90
+
91
+ # All BASE_HOT_WATER_PARAMS should be categorized into one of the groups
92
+ all_base_param_ids = set(BASE_HOT_WATER_PARAMS.keys())
93
+ assert all_grouped_params == all_base_param_ids
94
+
95
+
96
+ @pytest.mark.parametrize(
97
+ ("group1", "group2"),
98
+ [
99
+ (HOT_WATER_ESSENTIAL_PARAMS, HOT_WATER_CONFIG_PARAMS),
100
+ (HOT_WATER_ESSENTIAL_PARAMS, HOT_WATER_SCHEDULE_PARAMS),
101
+ (HOT_WATER_CONFIG_PARAMS, HOT_WATER_SCHEDULE_PARAMS),
102
+ ],
103
+ )
104
+ def test_hot_water_parameter_groups_no_overlap(
105
+ group1: set[str],
106
+ group2: set[str],
107
+ ) -> None:
108
+ """Test that hot water parameter groups don't overlap."""
109
+ assert not (group1 & group2), f"Groups should not overlap: {group1 & group2}"
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ ("group", "expected_count"),
114
+ [
115
+ (HOT_WATER_ESSENTIAL_PARAMS, 5), # Current optimized count
116
+ (HOT_WATER_CONFIG_PARAMS, 16), # Configuration parameters
117
+ (HOT_WATER_SCHEDULE_PARAMS, 8), # Time program parameters
118
+ ],
119
+ )
120
+ def test_hot_water_parameter_groups_expected_counts(
121
+ group: set[str],
122
+ expected_count: int,
123
+ ) -> None:
124
+ """Test that hot water parameter groups have expected counts."""
125
+ assert len(group) == expected_count
126
+
127
+
128
+ def test_hot_water_parameter_groups_total_count() -> None:
129
+ """Test that total grouped parameters match base parameters."""
130
+ total_grouped = (
131
+ len(HOT_WATER_ESSENTIAL_PARAMS)
132
+ + len(HOT_WATER_CONFIG_PARAMS)
133
+ + len(HOT_WATER_SCHEDULE_PARAMS)
134
+ )
135
+ assert total_grouped == len(BASE_HOT_WATER_PARAMS)
136
+
137
+
138
+ @pytest.mark.parametrize("version", ["v1", "v3"])
139
+ def test_api_config_structure(version: str) -> None:
140
+ """Test that API config has required structure."""
141
+ config = build_api_config(version)
142
+
143
+ # Check all required sections exist
144
+ required_sections = {"heating", "staticValues", "device", "sensor", "hot_water"}
145
+ assert set(config.keys()) == required_sections
146
+
147
+ # Check that each section is a dict with string keys and values
148
+ for section_name, section in config.items():
149
+ assert isinstance(section, dict)
150
+ for key, value in section.items():
151
+ assert isinstance(key, str), f"Key {key} in {section_name} not string"
152
+ assert isinstance(value, str), f"Value {value} in {section_name} not string"
@@ -12,7 +12,7 @@ import aiohttp
12
12
  import pytest
13
13
 
14
14
  from bsblan import BSBLAN, BSBLANConfig, HotWaterState
15
- from bsblan.constants import API_V3
15
+ from bsblan.constants import API_V3, APIConfig
16
16
  from bsblan.utility import APIValidator
17
17
 
18
18
  from . import load_fixture
@@ -32,7 +32,7 @@ async def test_hot_water_state(
32
32
  monkeypatch.setattr(bsblan, "_api_version", "v3")
33
33
 
34
34
  # Create a modified API_V3 excluding the time switch parameters
35
- test_api_v3 = {
35
+ test_api_v3: APIConfig = {
36
36
  "heating": API_V3["heating"].copy(),
37
37
  "staticValues": API_V3["staticValues"].copy(),
38
38
  "device": API_V3["device"].copy(),
@@ -54,12 +54,12 @@ async def test_hot_water_state(
54
54
  hot_water_cache = {
55
55
  "1600": "operating_mode",
56
56
  "1610": "nominal_setpoint",
57
- "1612": "reduced_setpoint",
58
57
  "1620": "release",
59
58
  "8830": "dhw_actual_value_top_temperature",
60
59
  "8820": "state_dhw_pump",
61
60
  # Add other parameters that would be in the full cache
62
61
  "1601": "eco_mode_selection",
62
+ "1612": "reduced_setpoint", # Now in config
63
63
  "1614": "nominal_setpoint_max",
64
64
  }
65
65
  bsblan.set_hot_water_cache(hot_water_cache)
@@ -92,23 +92,20 @@ async def test_hot_water_state(
92
92
  assert hot_water_state.operating_mode.value == 1
93
93
  assert hot_water_state.nominal_setpoint is not None
94
94
  assert hot_water_state.nominal_setpoint.value == 50.0
95
- assert hot_water_state.reduced_setpoint is not None
96
- assert hot_water_state.reduced_setpoint.value == 10.0
97
95
  assert hot_water_state.release is not None
98
96
  assert hot_water_state.release.value == 2
99
97
  assert hot_water_state.dhw_actual_value_top_temperature is not None
100
98
  assert hot_water_state.dhw_actual_value_top_temperature.value == 36.5
101
99
  assert hot_water_state.state_dhw_pump is not None
102
100
  assert hot_water_state.state_dhw_pump.value == 255
103
- # The Parameter string should only include the 6 essential parameters
101
+ # The Parameter string should only include the 5 essential parameters
104
102
  request_mock.assert_called_once()
105
103
  params = request_mock.call_args[1]["params"]["Parameter"].split(",")
106
- assert len(params) == 6
104
+ assert len(params) == 5
107
105
 
108
106
  expected_essential_params = [
109
107
  "1600", # operating_mode
110
108
  "1610", # nominal_setpoint
111
- "1612", # reduced_setpoint
112
109
  "1620", # release
113
110
  "8830", # dhw_actual_value_top_temperature
114
111
  "8820", # state_dhw_pump
@@ -53,7 +53,7 @@ async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
53
53
  base_path="/JS",
54
54
  data={
55
55
  "Parameter": "1601",
56
- "EnumValue": "1",
56
+ "Value": "1",
57
57
  "Type": "1",
58
58
  },
59
59
  )
@@ -63,7 +63,7 @@ async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
63
63
  base_path="/JS",
64
64
  data={
65
65
  "Parameter": "1630",
66
- "EnumValue": "1",
66
+ "Value": "1",
67
67
  "Type": "1",
68
68
  },
69
69
  )
@@ -83,7 +83,7 @@ async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
83
83
  base_path="/JS",
84
84
  data={
85
85
  "Parameter": "1647",
86
- "EnumValue": "1",
86
+ "Value": "1",
87
87
  "Type": "1",
88
88
  },
89
89
  )
@@ -103,7 +103,7 @@ async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
103
103
  base_path="/JS",
104
104
  data={
105
105
  "Parameter": "1660",
106
- "EnumValue": "2",
106
+ "Value": "2",
107
107
  "Type": "1",
108
108
  },
109
109
  )
@@ -133,7 +133,7 @@ async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
133
133
  base_path="/JS",
134
134
  data={
135
135
  "Parameter": "1680",
136
- "EnumValue": "1",
136
+ "Value": "1",
137
137
  "Type": "1",
138
138
  },
139
139
  )
@@ -188,7 +188,7 @@ async def test_prepare_hot_water_state(mock_bsblan: BSBLAN) -> None:
188
188
 
189
189
  assert state == {
190
190
  "Parameter": "1600",
191
- "EnumValue": "3",
191
+ "Value": "3",
192
192
  "Type": "1",
193
193
  }
194
194
 
@@ -201,7 +201,7 @@ async def test_prepare_hot_water_state(mock_bsblan: BSBLAN) -> None:
201
201
  )
202
202
  assert state == {
203
203
  "Parameter": "1601",
204
- "EnumValue": "1",
204
+ "Value": "1",
205
205
  "Type": "1",
206
206
  }
207
207
 
@@ -213,7 +213,7 @@ async def test_prepare_hot_water_state(mock_bsblan: BSBLAN) -> None:
213
213
  )
214
214
  assert state == {
215
215
  "Parameter": "1630",
216
- "EnumValue": "1",
216
+ "Value": "1",
217
217
  "Type": "1",
218
218
  }
219
219
 
@@ -237,7 +237,7 @@ async def test_prepare_hot_water_state(mock_bsblan: BSBLAN) -> None:
237
237
  )
238
238
  assert state == {
239
239
  "Parameter": "1647",
240
- "EnumValue": "1",
240
+ "Value": "1",
241
241
  "Type": "1",
242
242
  }
243
243
 
@@ -254,7 +254,7 @@ async def test_set_hot_water_state(
254
254
  """
255
255
  state = {
256
256
  "Parameter": "1600",
257
- "EnumValue": "3",
257
+ "Value": "3",
258
258
  "Type": "1",
259
259
  }
260
260
  await mock_bsblan._set_hot_water_state(state)
@@ -71,7 +71,7 @@ def create_response_handler(expected_data: dict[str, Any]) -> Response:
71
71
 
72
72
  for key, value in expected_data.items():
73
73
  assert key in actual_data, f"Expected key '{key}' not found in actual data"
74
- if key == "EnumValue":
74
+ if key == "Value":
75
75
  # Allow both string and integer representations
76
76
  assert str(actual_data[key]) == str(value)
77
77
  else:
@@ -116,7 +116,7 @@ async def test_change_hvac_mode(
116
116
  """Test changing BSBLAN HVAC mode."""
117
117
  expected_data = {
118
118
  "Parameter": "700",
119
- "EnumValue": 1, # 1 corresponds to "auto" mode
119
+ "Value": "1", # 1 corresponds to "auto" mode
120
120
  "Type": "1",
121
121
  }
122
122
  mock_aresponses.add(