python-bsblan 5.2.1__tar.gz → 6.0.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-5.2.1 → python_bsblan-6.0.1}/.github/workflows/codeql.yaml +2 -2
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/dependency-review.yaml +1 -1
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/release-drafter.yaml +1 -1
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/scorecard.yml +1 -1
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/tests.yaml +1 -1
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/PKG-INFO +2 -2
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/api/client.md +2 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/getting-started.md +36 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/control.py +34 -14
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/pyproject.toml +6 -6
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/bsblan.py +144 -22
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/constants.py +26 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/models.py +20 -0
- python_bsblan-6.0.1/tests/fixtures/pps_device.json +16 -0
- python_bsblan-6.0.1/tests/fixtures/pps_state.json +23 -0
- python_bsblan-6.0.1/tests/fixtures/pps_static_values.json +16 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_device.py +5 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_include_parameter.py +31 -0
- python_bsblan-6.0.1/tests/test_pps.py +380 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_time.py +13 -1
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.editorconfig +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.gitattributes +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/CODE_OF_CONDUCT.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/CONTRIBUTING.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/SECURITY.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/copilot-instructions.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/labels.yml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/prompts/add-parameter.prompt.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/prompts/code-review.prompt.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/release-drafter.yml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/renovate.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/skills/bsblan-parameters/SKILL.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/skills/bsblan-testing/SKILL.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/auto-approve-renovate.yml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/docs.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/labels.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/linting.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/lock.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/pr-labels.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/release.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/stale.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/workflows/typing.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.github/zizmor.yml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.gitignore +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.nvmrc +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.pre-commit-config.yaml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.prettierignore +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/.yamllint +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/AGENTS.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/CLAUDE.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/LICENSE.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/Makefile +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/README.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/api/constants.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/api/exceptions.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/api/models.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/docs/index.md +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/discovery.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/fetch_param.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/profile_init.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/ruff.toml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/examples/speed_test.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/mkdocs.yml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/package-lock.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/package.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/sonar-project.properties +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/__init__.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/exceptions.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/py.typed +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/src/bsblan/utility.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/__init__.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/conftest.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/device.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/dict_version.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/hot_water_state.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/info.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/password.txt +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/sensor.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/state.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/state_circuit2.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/static_state.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/static_state_circuit2.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/thermostat_hvac.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/thermostat_temp.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/fixtures/time.json +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/ruff.toml +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_api_initialization.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_api_validation.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_auth.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_backoff_retry.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_bsblan.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_bsblan_edge_cases.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_circuit.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_configuration.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_constants.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_context_manager.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_dhw_time_switch.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_entity_info.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_entity_info_ha.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_heating_schedule.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_hot_water_additional.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_hotwater_state.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_info.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_initialization.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_read_parameters.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_reset_validation.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_schedule_models.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_sensor.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_set_heating_schedule.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_set_hot_water_schedule.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_set_hotwater.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_state.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_static_state.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_temperature_unit.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_temperature_validation.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_thermostat.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_utility.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_utility_additional.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.1}/tests/test_utility_edge_cases.py +0 -0
- {python_bsblan-5.2.1 → python_bsblan-6.0.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@
|
|
30
|
+
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
|
31
31
|
- name: 🚀 Perform CodeQL Analysis
|
|
32
|
-
uses: github/codeql-action/analyze@
|
|
32
|
+
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
|
@@ -18,4 +18,4 @@ jobs:
|
|
|
18
18
|
persist-credentials: false
|
|
19
19
|
- name: 👀 Dependency review
|
|
20
20
|
# yamllint disable-line rule:line-length
|
|
21
|
-
uses: actions/dependency-review-action@
|
|
21
|
+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
@@ -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@
|
|
39
|
+
uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0
|
|
40
40
|
with:
|
|
41
41
|
prerelease: ${{ github.event.inputs.prerelease == 'true' }}
|
|
42
42
|
prerelease-identifier: ${{ github.event.inputs.prerelease_identifier }}
|
|
@@ -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@
|
|
69
|
+
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
|
70
70
|
with:
|
|
71
71
|
sarif_file: results.sarif
|
|
@@ -84,7 +84,7 @@ jobs:
|
|
|
84
84
|
uv run python -m coverage xml -i
|
|
85
85
|
- name: 🚀 Upload coverage report
|
|
86
86
|
if: env.HAS_CODECOV_TOKEN == 'true'
|
|
87
|
-
uses: codecov/codecov-action@
|
|
87
|
+
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
88
88
|
with:
|
|
89
89
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
90
90
|
- name: ℹ️ Skip Codecov upload (missing token)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-bsblan
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.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
|
|
@@ -24,9 +24,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
26
|
Requires-Dist: aiohttp>=3.8.1
|
|
27
|
-
Requires-Dist: backoff>=2.2.1
|
|
28
27
|
Requires-Dist: packaging>=21.3
|
|
29
28
|
Requires-Dist: pydantic>=2.0
|
|
29
|
+
Requires-Dist: python-backoff>=2.3.1
|
|
30
30
|
Requires-Dist: yarl>=1.7.2
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
@@ -63,6 +63,42 @@ async def main() -> None:
|
|
|
63
63
|
asyncio.run(main())
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## PPS bus support
|
|
67
|
+
|
|
68
|
+
PPS bus devices are detected from the device metadata returned by BSB-LAN. The
|
|
69
|
+
client provides minimal climate support for PPS devices through the same climate
|
|
70
|
+
methods used by BSB/LPB devices.
|
|
71
|
+
|
|
72
|
+
Supported PPS climate operations:
|
|
73
|
+
|
|
74
|
+
- `state()` for `hvac_mode`, `target_temperature`, and `current_temperature`
|
|
75
|
+
- `static_values()` for `min_temp` and `max_temp`
|
|
76
|
+
- `thermostat()` for target temperature and HVAC mode
|
|
77
|
+
- `get_available_circuits()`, which returns `[1]` when the single PPS
|
|
78
|
+
climate circuit is available, otherwise `[]`; PPS devices only ever expose
|
|
79
|
+
circuit `1`
|
|
80
|
+
|
|
81
|
+
PPS devices currently have these limitations:
|
|
82
|
+
|
|
83
|
+
- Only circuit `1` is supported.
|
|
84
|
+
- `time()` and `set_time()` are not supported for PPS devices.
|
|
85
|
+
- `thermostat(hvac_mode=2)` is not supported on PPS devices. Valid PPS modes
|
|
86
|
+
are `0` (off), `1` (auto), and `3` (heat/manual).
|
|
87
|
+
- Hot water and schedule helpers are intended for BSB/LPB devices.
|
|
88
|
+
|
|
89
|
+
Check `supports_time_sync` before showing or calling time synchronization in
|
|
90
|
+
applications:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
async with BSBLAN(config) as client:
|
|
94
|
+
device = client.device_info or await client.device()
|
|
95
|
+
print(f"Bus type: {device.bus or 'unknown'}")
|
|
96
|
+
|
|
97
|
+
if client.supports_time_sync:
|
|
98
|
+
device_time = await client.time()
|
|
99
|
+
print(device_time.time.value)
|
|
100
|
+
```
|
|
101
|
+
|
|
66
102
|
## Hot water control
|
|
67
103
|
|
|
68
104
|
```python
|
|
@@ -68,6 +68,16 @@ def print_attributes(title: str, attributes: dict[str, str]) -> None:
|
|
|
68
68
|
print(f"{label}: {value}")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def format_yes_no(*, value: bool) -> str:
|
|
72
|
+
"""Format a boolean as a readable yes/no value."""
|
|
73
|
+
return "yes" if value else "no"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_optional(value: Any) -> str:
|
|
77
|
+
"""Format optional device metadata for display."""
|
|
78
|
+
return "N/A" if value is None else str(value)
|
|
79
|
+
|
|
80
|
+
|
|
71
81
|
def get_hvac_action_name(status_code: int) -> str:
|
|
72
82
|
"""Map BSB-LAN parameter 8000 status code to a human-readable HVAC action.
|
|
73
83
|
|
|
@@ -180,6 +190,11 @@ async def print_device_info(device: Device, info: Info) -> None:
|
|
|
180
190
|
"Device Name": device.name or "N/A",
|
|
181
191
|
"Version": device.version or "N/A",
|
|
182
192
|
"Device Identification": device_identification,
|
|
193
|
+
"Bus Type": format_optional(device.bus),
|
|
194
|
+
"Bus Writable Flag": format_optional(device.buswritable),
|
|
195
|
+
"Bus Address": format_optional(device.busaddr),
|
|
196
|
+
"Bus Destination": format_optional(device.busdest),
|
|
197
|
+
"Supports Time Sync": format_yes_no(value=device.supports_time_sync),
|
|
183
198
|
}
|
|
184
199
|
print_attributes("Device Information", attributes)
|
|
185
200
|
|
|
@@ -319,6 +334,11 @@ async def main() -> None:
|
|
|
319
334
|
|
|
320
335
|
# Initialize BSBLAN with the configuration object
|
|
321
336
|
async with BSBLAN(config) as bsblan:
|
|
337
|
+
# Get and print device and general info, including bus metadata
|
|
338
|
+
device: Device = bsblan.device_info or await bsblan.device()
|
|
339
|
+
info: Info = await bsblan.info()
|
|
340
|
+
await print_device_info(device, info)
|
|
341
|
+
|
|
322
342
|
# Get and print state
|
|
323
343
|
state: State = await bsblan.state()
|
|
324
344
|
await print_state(state)
|
|
@@ -335,14 +355,12 @@ async def main() -> None:
|
|
|
335
355
|
sensor: Sensor = await bsblan.sensor()
|
|
336
356
|
await print_sensor(sensor)
|
|
337
357
|
|
|
338
|
-
# Get and print device and general info
|
|
339
|
-
device: Device = await bsblan.device()
|
|
340
|
-
info: Info = await bsblan.info()
|
|
341
|
-
await print_device_info(device, info)
|
|
342
|
-
|
|
343
358
|
# Get and print device time
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
if bsblan.supports_time_sync:
|
|
360
|
+
device_time: DeviceTime = await bsblan.time()
|
|
361
|
+
await print_device_time(device_time)
|
|
362
|
+
else:
|
|
363
|
+
print("\nDevice time is not available for this bus type")
|
|
346
364
|
|
|
347
365
|
# Get and print static state
|
|
348
366
|
static_state: StaticState = await bsblan.static_values()
|
|
@@ -375,13 +393,15 @@ async def main() -> None:
|
|
|
375
393
|
await bsblan.set_hot_water(SetHotWaterParam(dhw_time_programs=dhw_programs))
|
|
376
394
|
|
|
377
395
|
# Example: Set device time
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
396
|
+
if bsblan.supports_time_sync:
|
|
397
|
+
print("\nSetting device time to current system time")
|
|
398
|
+
# Get current local system time and format it for BSB-LAN.
|
|
399
|
+
current_time = datetime.now().replace(microsecond=0) # noqa: DTZ005
|
|
400
|
+
formatted_time = current_time.strftime("%d.%m.%Y %H:%M:%S")
|
|
401
|
+
print(f"Current system time: {formatted_time}")
|
|
402
|
+
await bsblan.set_time(formatted_time)
|
|
403
|
+
else:
|
|
404
|
+
print("\nSkipping device time sync for this bus type")
|
|
385
405
|
|
|
386
406
|
|
|
387
407
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-bsblan"
|
|
3
|
-
version = "
|
|
3
|
+
version = "6.0.1"
|
|
4
4
|
description = "Asynchronous Python client for BSBLAN API"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Willem-Jan van Rootselaar", email = "liudgervr@gmail.com"}
|
|
@@ -28,7 +28,7 @@ dependencies = [
|
|
|
28
28
|
"aiohttp>=3.8.1",
|
|
29
29
|
"yarl>=1.7.2",
|
|
30
30
|
"packaging>=21.3",
|
|
31
|
-
"backoff>=2.
|
|
31
|
+
"python-backoff>=2.3.1",
|
|
32
32
|
"pydantic>=2.0",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -185,18 +185,18 @@ build-backend = "hatchling.build"
|
|
|
185
185
|
dev = [
|
|
186
186
|
"aresponses==3.0.0",
|
|
187
187
|
"bandit==1.9.4",
|
|
188
|
-
"black==26.
|
|
188
|
+
"black==26.5.1",
|
|
189
189
|
"blacken-docs==1.20.0",
|
|
190
190
|
"codespell==2.4.2",
|
|
191
191
|
"covdefaults==2.3.0",
|
|
192
|
-
"coverage==7.
|
|
192
|
+
"coverage==7.14.0",
|
|
193
193
|
"darglint==1.8.1",
|
|
194
194
|
"flake8==7.3.0",
|
|
195
195
|
"flake8-simplify==0.30.0",
|
|
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.
|
|
199
|
+
"ty==0.0.38",
|
|
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.
|
|
208
|
+
"ruff==0.15.13",
|
|
209
209
|
"safety==3.7.0",
|
|
210
210
|
"vulture==2.16",
|
|
211
211
|
"yamllint==1.38.0",
|
|
@@ -19,6 +19,8 @@ from yarl import URL
|
|
|
19
19
|
|
|
20
20
|
from .constants import (
|
|
21
21
|
API_VERSIONS,
|
|
22
|
+
PPS_HEATING_PARAMS,
|
|
23
|
+
PPS_STATIC_VALUES_PARAMS,
|
|
22
24
|
APIConfig,
|
|
23
25
|
CircuitConfig,
|
|
24
26
|
ErrorMsg,
|
|
@@ -100,6 +102,7 @@ class BSBLAN:
|
|
|
100
102
|
_firmware_version: str | None = None
|
|
101
103
|
_api_version: str | None = None
|
|
102
104
|
_api_data: APIConfig | None = None
|
|
105
|
+
_device: Device | None = None
|
|
103
106
|
_initialized: bool = False
|
|
104
107
|
_api_validator: APIValidator = field(init=False)
|
|
105
108
|
_temperature_unit: str = "°C"
|
|
@@ -170,6 +173,9 @@ class BSBLAN:
|
|
|
170
173
|
# circuits == [1, 2] for a dual-circuit controller
|
|
171
174
|
|
|
172
175
|
"""
|
|
176
|
+
if self._uses_pps_bus:
|
|
177
|
+
return await self._get_available_pps_circuits()
|
|
178
|
+
|
|
173
179
|
available: list[int] = []
|
|
174
180
|
for circuit, param_id in CircuitConfig.PROBE_PARAMS.items():
|
|
175
181
|
try:
|
|
@@ -195,6 +201,20 @@ class BSBLAN:
|
|
|
195
201
|
available.append(circuit)
|
|
196
202
|
return sorted(available)
|
|
197
203
|
|
|
204
|
+
async def _get_available_pps_circuits(self) -> list[int]:
|
|
205
|
+
"""Detect the single PPS room-unit climate circuit."""
|
|
206
|
+
param_id = "15000"
|
|
207
|
+
try:
|
|
208
|
+
response = await self._request(params={"Parameter": param_id})
|
|
209
|
+
except BSBLANError:
|
|
210
|
+
logger.debug("PPS climate circuit not available")
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
if not response.get(param_id):
|
|
214
|
+
logger.debug("PPS climate circuit has no operating mode data")
|
|
215
|
+
return []
|
|
216
|
+
return [1]
|
|
217
|
+
|
|
198
218
|
async def _setup_api_validator(self) -> None:
|
|
199
219
|
"""Set up the API validator without validating sections.
|
|
200
220
|
|
|
@@ -208,9 +228,21 @@ class BSBLAN:
|
|
|
208
228
|
if self._api_data is None:
|
|
209
229
|
self._api_data = self._copy_api_config()
|
|
210
230
|
|
|
231
|
+
self._apply_bus_specific_api_config()
|
|
232
|
+
|
|
211
233
|
# Initialize the API validator (but don't validate sections yet)
|
|
212
234
|
self._api_validator = APIValidator(self._api_data)
|
|
213
235
|
|
|
236
|
+
def _apply_bus_specific_api_config(self) -> None:
|
|
237
|
+
"""Apply bus-specific parameter maps to the current API config."""
|
|
238
|
+
if self._api_data is None or not self._uses_pps_bus:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
self._api_data["heating"] = PPS_HEATING_PARAMS.copy()
|
|
242
|
+
self._api_data["staticValues"] = PPS_STATIC_VALUES_PARAMS.copy()
|
|
243
|
+
self._api_data["heating_circuit2"] = {}
|
|
244
|
+
self._api_data["staticValues_circuit2"] = {}
|
|
245
|
+
|
|
214
246
|
async def _ensure_section_validated(
|
|
215
247
|
self, section: SectionLiteral, include: list[str] | None = None
|
|
216
248
|
) -> None:
|
|
@@ -247,11 +279,26 @@ class BSBLAN:
|
|
|
247
279
|
logger.debug("Lazy loading section: %s", section)
|
|
248
280
|
response_data = await self._validate_api_section(section, include)
|
|
249
281
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
if response_data and self._should_extract_temperature_unit(
|
|
283
|
+
section, include, response_data
|
|
284
|
+
):
|
|
253
285
|
self._extract_temperature_unit_from_response(response_data)
|
|
254
286
|
|
|
287
|
+
def _should_extract_temperature_unit(
|
|
288
|
+
self,
|
|
289
|
+
section: SectionLiteral,
|
|
290
|
+
include: list[str] | None,
|
|
291
|
+
response_data: dict[str, Any],
|
|
292
|
+
) -> bool:
|
|
293
|
+
"""Return whether the validation response should update temperature unit."""
|
|
294
|
+
if section != "heating":
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
if include is None or "target_temperature" in include:
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
return any(param_id in response_data for param_id in ("710", "15004"))
|
|
301
|
+
|
|
255
302
|
async def _ensure_hot_water_group_validated(
|
|
256
303
|
self,
|
|
257
304
|
group_name: str,
|
|
@@ -444,17 +491,16 @@ class BSBLAN:
|
|
|
444
491
|
) -> None:
|
|
445
492
|
"""Extract temperature unit from heating section response data.
|
|
446
493
|
|
|
447
|
-
Gets the unit from
|
|
494
|
+
Gets the unit from the target_temperature parameter, which is always
|
|
448
495
|
present in the heating section.
|
|
449
496
|
|
|
450
497
|
Args:
|
|
451
498
|
response_data: The response data from heating section validation
|
|
452
499
|
|
|
453
500
|
"""
|
|
454
|
-
# Look for
|
|
501
|
+
# Look for target_temperature in the response.
|
|
455
502
|
for param_id, param_data in response_data.items():
|
|
456
|
-
|
|
457
|
-
if param_id == "710" and isinstance(param_data, dict):
|
|
503
|
+
if param_id in {"710", "15004"} and isinstance(param_data, dict):
|
|
458
504
|
unit = param_data.get("unit", "")
|
|
459
505
|
if unit in ("°C", "°C"):
|
|
460
506
|
self._temperature_unit = "°C"
|
|
@@ -463,16 +509,15 @@ class BSBLAN:
|
|
|
463
509
|
else:
|
|
464
510
|
# Keep default if unit is empty or unknown
|
|
465
511
|
logger.debug(
|
|
466
|
-
"Unknown or empty temperature unit from
|
|
467
|
-
"Using default (°C)",
|
|
512
|
+
"Unknown or empty temperature unit from heating target: "
|
|
513
|
+
"'%s'. Using default (°C)",
|
|
468
514
|
unit,
|
|
469
515
|
)
|
|
470
516
|
logger.debug("Temperature unit set to: %s", self._temperature_unit)
|
|
471
517
|
return
|
|
472
518
|
|
|
473
|
-
# If we didn't find parameter 710, log a warning
|
|
474
519
|
logger.warning(
|
|
475
|
-
"Could not find
|
|
520
|
+
"Could not find target temperature in heating section response. "
|
|
476
521
|
"Using default temperature unit (°C)"
|
|
477
522
|
)
|
|
478
523
|
|
|
@@ -494,6 +539,31 @@ class BSBLAN:
|
|
|
494
539
|
logger.debug("BSBLAN version: %s", self._firmware_version)
|
|
495
540
|
self._set_api_version()
|
|
496
541
|
|
|
542
|
+
@property
|
|
543
|
+
def device_info(self) -> Device | None:
|
|
544
|
+
"""Return cached device metadata from the last /JI response."""
|
|
545
|
+
return self._device
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def supports_time_sync(self) -> bool:
|
|
549
|
+
"""Return cached support for the normal BSB/LPB time sync command."""
|
|
550
|
+
return self._device is not None and self._device.supports_time_sync
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def _uses_pps_bus(self) -> bool:
|
|
554
|
+
"""Return whether cached metadata identifies the device as PPS."""
|
|
555
|
+
return self._device is not None and self._device.is_pps_bus
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def _is_bus_writable(self) -> bool:
|
|
559
|
+
"""Return whether cached metadata says writes are allowed."""
|
|
560
|
+
return self._device is None or self._device.is_bus_writable
|
|
561
|
+
|
|
562
|
+
async def _ensure_device_metadata(self) -> None:
|
|
563
|
+
"""Fetch device metadata if it has not been loaded yet."""
|
|
564
|
+
if self._device is None:
|
|
565
|
+
await self.device()
|
|
566
|
+
|
|
497
567
|
def _set_api_version(self) -> None:
|
|
498
568
|
"""Set the API version based on the firmware version.
|
|
499
569
|
|
|
@@ -572,8 +642,8 @@ class BSBLAN:
|
|
|
572
642
|
Args:
|
|
573
643
|
circuit: The heating circuit number (1 or 2).
|
|
574
644
|
|
|
575
|
-
Note: Temperature unit is extracted during heating section validation
|
|
576
|
-
|
|
645
|
+
Note: Temperature unit is extracted during heating section validation,
|
|
646
|
+
so no extra API call is needed here.
|
|
577
647
|
|
|
578
648
|
"""
|
|
579
649
|
if circuit in self._circuit_temp_initialized:
|
|
@@ -593,10 +663,20 @@ class BSBLAN:
|
|
|
593
663
|
BSBLANInvalidParameterError: If the circuit number is invalid.
|
|
594
664
|
|
|
595
665
|
"""
|
|
596
|
-
if circuit not in CircuitConfig.VALID:
|
|
666
|
+
if circuit not in CircuitConfig.VALID or (self._uses_pps_bus and circuit != 1):
|
|
597
667
|
msg = ErrorMsg.INVALID_CIRCUIT.format(circuit)
|
|
598
668
|
raise BSBLANInvalidParameterError(msg)
|
|
599
669
|
|
|
670
|
+
def _validate_bus_write_supported(self) -> None:
|
|
671
|
+
"""Validate that cached metadata permits writes."""
|
|
672
|
+
if not self._is_bus_writable:
|
|
673
|
+
raise BSBLANError(ErrorMsg.BUS_WRITE_NOT_SUPPORTED)
|
|
674
|
+
|
|
675
|
+
def _validate_time_sync_supported(self) -> None:
|
|
676
|
+
"""Validate that normal parameter 0 time sync is safe."""
|
|
677
|
+
if not self.supports_time_sync:
|
|
678
|
+
raise BSBLANError(ErrorMsg.TIME_SYNC_NOT_SUPPORTED)
|
|
679
|
+
|
|
600
680
|
@property
|
|
601
681
|
def get_temperature_unit(self) -> str:
|
|
602
682
|
"""Get the unit of temperature.
|
|
@@ -878,8 +958,26 @@ class BSBLAN:
|
|
|
878
958
|
params = self._extract_params_summary(section_params)
|
|
879
959
|
data = await self._request(params={"Parameter": params["string_par"]})
|
|
880
960
|
data = dict(zip(params["list"], list(data.values()), strict=True))
|
|
961
|
+
if section == "heating" and self._uses_pps_bus:
|
|
962
|
+
self._normalize_pps_state_data(data)
|
|
881
963
|
return model_class.model_validate(data)
|
|
882
964
|
|
|
965
|
+
def _normalize_pps_state_data(self, data: dict[str, Any]) -> None:
|
|
966
|
+
"""Normalize PPS climate values to the library's State model."""
|
|
967
|
+
hvac_mode = data.get("hvac_mode")
|
|
968
|
+
if not isinstance(hvac_mode, dict):
|
|
969
|
+
return
|
|
970
|
+
|
|
971
|
+
try:
|
|
972
|
+
raw_mode = int(hvac_mode["value"])
|
|
973
|
+
except (KeyError, TypeError, ValueError):
|
|
974
|
+
return
|
|
975
|
+
|
|
976
|
+
hvac_mode["value"] = Validation.PPS_HVAC_MODE_FROM_BSBLAN.get(
|
|
977
|
+
raw_mode,
|
|
978
|
+
raw_mode,
|
|
979
|
+
)
|
|
980
|
+
|
|
883
981
|
async def state(
|
|
884
982
|
self,
|
|
885
983
|
include: list[str] | None = None,
|
|
@@ -901,8 +999,9 @@ class BSBLAN:
|
|
|
901
999
|
State: The current state of the BSBLAN device.
|
|
902
1000
|
|
|
903
1001
|
Note:
|
|
904
|
-
|
|
905
|
-
0=off, 1=auto, 2=eco, 3=heat.
|
|
1002
|
+
For BSB/LPB devices, hvac_mode.value is returned as a raw integer:
|
|
1003
|
+
0=off, 1=auto, 2=eco, 3=heat. PPS devices normalize their raw
|
|
1004
|
+
operating modes to the same library values, but do not support eco.
|
|
906
1005
|
|
|
907
1006
|
Example:
|
|
908
1007
|
# Fetch only hvac_mode and current_temperature
|
|
@@ -974,7 +1073,8 @@ class BSBLAN:
|
|
|
974
1073
|
|
|
975
1074
|
"""
|
|
976
1075
|
device_info = await self._request(base_path="/JI")
|
|
977
|
-
|
|
1076
|
+
self._device = Device.model_validate(device_info)
|
|
1077
|
+
return self._device
|
|
978
1078
|
|
|
979
1079
|
async def info(self, include: list[str] | None = None) -> Info:
|
|
980
1080
|
"""Get information about the current heating system config.
|
|
@@ -1001,6 +1101,9 @@ class BSBLAN:
|
|
|
1001
1101
|
DeviceTime: The current time information from the BSB-LAN device.
|
|
1002
1102
|
|
|
1003
1103
|
"""
|
|
1104
|
+
await self._ensure_device_metadata()
|
|
1105
|
+
self._validate_time_sync_supported()
|
|
1106
|
+
|
|
1004
1107
|
# Get only parameter 0 for time
|
|
1005
1108
|
data = await self._request(params={"Parameter": "0"})
|
|
1006
1109
|
# Create the data dictionary in the expected format
|
|
@@ -1018,6 +1121,8 @@ class BSBLAN:
|
|
|
1018
1121
|
BSBLANInvalidParameterError: If the time format is invalid.
|
|
1019
1122
|
|
|
1020
1123
|
"""
|
|
1124
|
+
await self._ensure_device_metadata()
|
|
1125
|
+
self._validate_time_sync_supported()
|
|
1021
1126
|
self._validate_time_format(time_value)
|
|
1022
1127
|
state: dict[str, object] = {
|
|
1023
1128
|
"Parameter": "0",
|
|
@@ -1038,7 +1143,9 @@ class BSBLAN:
|
|
|
1038
1143
|
Args:
|
|
1039
1144
|
target_temperature (str | None): The target temperature to set.
|
|
1040
1145
|
hvac_mode (int | None): The HVAC mode to set as raw integer value.
|
|
1041
|
-
|
|
1146
|
+
For BSB/LPB, valid values are 0=off, 1=auto, 2=eco, 3=heat.
|
|
1147
|
+
For PPS, valid values are 0=off, 1=auto, and 3=heat/manual;
|
|
1148
|
+
they are translated to PPS raw values before posting.
|
|
1042
1149
|
circuit: The heating circuit number (1 or 2). Defaults to 1.
|
|
1043
1150
|
|
|
1044
1151
|
Example:
|
|
@@ -1050,6 +1157,8 @@ class BSBLAN:
|
|
|
1050
1157
|
|
|
1051
1158
|
"""
|
|
1052
1159
|
self._validate_circuit(circuit)
|
|
1160
|
+
if self._uses_pps_bus:
|
|
1161
|
+
self._validate_bus_write_supported()
|
|
1053
1162
|
await self._initialize_temperature_range(circuit)
|
|
1054
1163
|
|
|
1055
1164
|
self._validate_single_parameter(
|
|
@@ -1082,7 +1191,7 @@ class BSBLAN:
|
|
|
1082
1191
|
dict[str, Any]: The prepared state for the thermostat.
|
|
1083
1192
|
|
|
1084
1193
|
"""
|
|
1085
|
-
param_ids =
|
|
1194
|
+
param_ids = self._thermostat_params(circuit)
|
|
1086
1195
|
state: dict[str, Any] = {}
|
|
1087
1196
|
if target_temperature is not None:
|
|
1088
1197
|
await self._validate_target_temperature(
|
|
@@ -1098,15 +1207,24 @@ class BSBLAN:
|
|
|
1098
1207
|
)
|
|
1099
1208
|
if hvac_mode is not None:
|
|
1100
1209
|
self._validate_hvac_mode(hvac_mode)
|
|
1210
|
+
hvac_value = str(hvac_mode)
|
|
1211
|
+
if self._uses_pps_bus:
|
|
1212
|
+
hvac_value = Validation.PPS_HVAC_MODE_TO_BSBLAN[hvac_mode]
|
|
1101
1213
|
state.update(
|
|
1102
1214
|
{
|
|
1103
1215
|
"Parameter": param_ids["hvac_mode"],
|
|
1104
|
-
"Value":
|
|
1216
|
+
"Value": hvac_value,
|
|
1105
1217
|
"Type": "1",
|
|
1106
1218
|
},
|
|
1107
1219
|
)
|
|
1108
1220
|
return state
|
|
1109
1221
|
|
|
1222
|
+
def _thermostat_params(self, circuit: int) -> dict[str, str]:
|
|
1223
|
+
"""Return thermostat write parameters for the active bus type."""
|
|
1224
|
+
if self._uses_pps_bus:
|
|
1225
|
+
return {"target_temperature": "15004", "hvac_mode": "15000"}
|
|
1226
|
+
return CircuitConfig.THERMOSTAT_PARAMS[circuit]
|
|
1227
|
+
|
|
1110
1228
|
async def _validate_target_temperature(
|
|
1111
1229
|
self,
|
|
1112
1230
|
target_temperature: str,
|
|
@@ -1151,13 +1269,17 @@ class BSBLAN:
|
|
|
1151
1269
|
"""Validate the HVAC mode.
|
|
1152
1270
|
|
|
1153
1271
|
Args:
|
|
1154
|
-
hvac_mode (int): The HVAC mode to validate
|
|
1272
|
+
hvac_mode (int): The HVAC mode to validate. BSB/LPB accepts 0-3;
|
|
1273
|
+
PPS accepts 0, 1, and 3.
|
|
1155
1274
|
|
|
1156
1275
|
Raises:
|
|
1157
1276
|
BSBLANInvalidParameterError: If the HVAC mode is invalid.
|
|
1158
1277
|
|
|
1159
1278
|
"""
|
|
1160
|
-
|
|
1279
|
+
valid_modes = (
|
|
1280
|
+
Validation.PPS_HVAC_MODES if self._uses_pps_bus else Validation.HVAC_MODES
|
|
1281
|
+
)
|
|
1282
|
+
if hvac_mode not in valid_modes:
|
|
1161
1283
|
raise BSBLANInvalidParameterError(str(hvac_mode))
|
|
1162
1284
|
|
|
1163
1285
|
def _validate_time_format(self, time_value: str) -> None:
|
|
@@ -114,6 +114,19 @@ BASE_STATIC_VALUES_CIRCUIT2_PARAMS: Final[dict[str, str]] = {
|
|
|
114
114
|
"1014": "min_temp",
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
# PPS bus supports one room-unit style climate circuit. These parameters are
|
|
118
|
+
# exposed by BSB-LAN in the 15000+ range and mirror the circuit 1 climate model.
|
|
119
|
+
PPS_HEATING_PARAMS: Final[dict[str, str]] = {
|
|
120
|
+
"15000": "hvac_mode",
|
|
121
|
+
"15004": "target_temperature",
|
|
122
|
+
"15008": "current_temperature",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
PPS_STATIC_VALUES_PARAMS: Final[dict[str, str]] = {
|
|
126
|
+
"15006": "min_temp",
|
|
127
|
+
"15007": "max_temp",
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
V1_STATIC_VALUES_CIRCUIT2_EXTENSIONS: Final[dict[str, str]] = {
|
|
118
131
|
"1030": "max_temp",
|
|
119
132
|
}
|
|
@@ -207,6 +220,17 @@ class Validation:
|
|
|
207
220
|
"""Validation-related constants for BSBLAN."""
|
|
208
221
|
|
|
209
222
|
HVAC_MODES: Final[set[int]] = {0, 1, 2, 3}
|
|
223
|
+
PPS_HVAC_MODES: Final[set[int]] = {0, 1, 3}
|
|
224
|
+
PPS_HVAC_MODE_TO_BSBLAN: Final[dict[int, str]] = {
|
|
225
|
+
0: "2", # off
|
|
226
|
+
1: "0", # auto
|
|
227
|
+
3: "1", # manual/comfort
|
|
228
|
+
}
|
|
229
|
+
PPS_HVAC_MODE_FROM_BSBLAN: Final[dict[int, int]] = {
|
|
230
|
+
0: 1, # auto
|
|
231
|
+
1: 3, # manual/comfort
|
|
232
|
+
2: 0, # off
|
|
233
|
+
}
|
|
210
234
|
MIN_YEAR: Final[int] = 1900
|
|
211
235
|
MAX_YEAR: Final[int] = 2100
|
|
212
236
|
|
|
@@ -492,6 +516,8 @@ class ErrorMsg:
|
|
|
492
516
|
"Empty include list provided. Use None to fetch all parameters."
|
|
493
517
|
)
|
|
494
518
|
NO_HEATING_SCHEDULE_PARAMS = "No heating schedule parameters available"
|
|
519
|
+
TIME_SYNC_NOT_SUPPORTED = "Time synchronization is not supported by this device"
|
|
520
|
+
BUS_WRITE_NOT_SUPPORTED = "Writing parameters is not supported by this device"
|
|
495
521
|
|
|
496
522
|
|
|
497
523
|
# Handle both ASCII and Unicode degree symbols
|
|
@@ -611,6 +611,26 @@ class Device(BaseModel):
|
|
|
611
611
|
version: str
|
|
612
612
|
MAC: str # pylint: disable=invalid-name
|
|
613
613
|
uptime: int
|
|
614
|
+
bus: str | None = None
|
|
615
|
+
buswritable: int | bool | None = None
|
|
616
|
+
busaddr: int | None = None
|
|
617
|
+
busdest: int | None = None
|
|
618
|
+
busdevices: list[Any] | None = None
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def is_pps_bus(self) -> bool:
|
|
622
|
+
"""Return whether the device is connected to a PPS bus."""
|
|
623
|
+
return self.bus is not None and self.bus.upper() == "PPS"
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def is_bus_writable(self) -> bool:
|
|
627
|
+
"""Return whether BSB-LAN reports global bus writes as enabled."""
|
|
628
|
+
return self.buswritable is None or bool(self.buswritable)
|
|
629
|
+
|
|
630
|
+
@property
|
|
631
|
+
def supports_time_sync(self) -> bool:
|
|
632
|
+
"""Return whether normal BSB/LPB time synchronization is supported."""
|
|
633
|
+
return not self.is_pps_bus
|
|
614
634
|
|
|
615
635
|
|
|
616
636
|
class Info(BaseModel):
|