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.
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/codeql.yaml +3 -3
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/labels.yaml +1 -1
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/linting.yaml +12 -12
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/release.yaml +3 -3
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/stale.yaml +1 -1
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/tests.yaml +5 -5
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/typing.yaml +2 -2
- python_bsblan-3.1.1/.nvmrc +1 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/PKG-INFO +1 -1
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/examples/control.py +9 -9
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/pyproject.toml +24 -11
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/bsblan.py +114 -29
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/models.py +3 -3
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/thermostat_hvac.json +1 -1
- python_bsblan-3.1.1/tests/test_api_initialization.py +103 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_api_validation.py +66 -1
- python_bsblan-3.1.1/tests/test_constants.py +152 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_hotwater_state.py +5 -8
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_initialization.py +16 -4
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_set_hotwater.py +10 -10
- python_bsblan-3.1.1/tests/test_temperature_unit.py +211 -0
- python_bsblan-3.1.1/tests/test_temperature_validation.py +166 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_thermostat.py +2 -2
- python_bsblan-3.1.1/uv.lock +2656 -0
- python_bsblan-3.0.0/.nvmrc +0 -1
- python_bsblan-3.0.0/tests/test_api_initialization.py +0 -50
- python_bsblan-3.0.0/tests/test_temperature_unit.py +0 -97
- python_bsblan-3.0.0/tests/test_temperature_validation.py +0 -64
- python_bsblan-3.0.0/uv.lock +0 -2316
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.editorconfig +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.gitattributes +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/CODE_OF_CONDUCT.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/CONTRIBUTING.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/LICENSE.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/labels.yml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/release-drafter.yml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/renovate.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/lock.yaml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/pr-labels.yaml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.github/workflows/release-drafter.yaml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.gitignore +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.pre-commit-config.yaml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.prettierignore +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/.yamllint +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/README.md +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/examples/ruff.toml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/package-lock.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/package.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/sonar-project.properties +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/__init__.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/constants.py +1 -1
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/exceptions.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/py.typed +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/src/bsblan/utility.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/__init__.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/conftest.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/device.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/dict_version.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/hot_water_state.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/info.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/password.txt +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/sensor.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/state.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/static_state.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/thermostat_temp.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/fixtures/time.json +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/ruff.toml +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_auth.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_bsblan.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_bsblan_edge_cases.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_configuration.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_context_manager.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_device.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_dhw_time_switch.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_entity_info.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_hot_water_additional.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_info.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_reset_validation.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_sensor.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_state.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_static_state.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_time.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility_additional.py +0 -0
- {python_bsblan-3.0.0 → python_bsblan-3.1.1}/tests/test_utility_edge_cases.py +0 -0
- {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@
|
|
20
|
+
uses: actions/checkout@v5.0.0
|
|
21
21
|
- name: 🏗 Initialize CodeQL
|
|
22
|
-
uses: github/codeql-action/init@v3.
|
|
22
|
+
uses: github/codeql-action/init@v3.30.3
|
|
23
23
|
- name: 🚀 Perform CodeQL Analysis
|
|
24
|
-
uses: github/codeql-action/analyze@v3.
|
|
24
|
+
uses: github/codeql-action/analyze@v3.30.3
|
|
@@ -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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@v1.13.0
|
|
47
47
|
with:
|
|
48
48
|
verbose: true
|
|
49
49
|
print-hash: true
|
|
@@ -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@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
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@
|
|
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@
|
|
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
|
|
@@ -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
|
-
- HotWaterConfig: Configuration parameters checked less frequently (
|
|
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
|
|
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.
|
|
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.
|
|
168
|
-
"blacken-docs==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.
|
|
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.
|
|
191
|
+
"mypy==1.18.2",
|
|
179
192
|
"pre-commit==4.3.0",
|
|
180
193
|
"pre-commit-hooks==6.0.0",
|
|
181
|
-
"pylint==3.3.
|
|
194
|
+
"pylint==3.3.9",
|
|
182
195
|
"pytest>=8.3.5",
|
|
183
|
-
"pytest-asyncio==1.
|
|
184
|
-
"pytest-cov==
|
|
196
|
+
"pytest-asyncio==1.3.0",
|
|
197
|
+
"pytest-cov==7.0.0",
|
|
185
198
|
"pyupgrade==3.20.0",
|
|
186
|
-
"ruff==0.
|
|
187
|
-
"safety==3.6.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|