python-bsblan 5.2.0__py3-none-any.whl → 6.0.0__py3-none-any.whl
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.
- bsblan/bsblan.py +160 -64
- bsblan/constants.py +26 -0
- bsblan/models.py +23 -3
- {python_bsblan-5.2.0.dist-info → python_bsblan-6.0.0.dist-info}/METADATA +1 -1
- python_bsblan-6.0.0.dist-info/RECORD +11 -0
- python_bsblan-5.2.0.dist-info/RECORD +0 -11
- {python_bsblan-5.2.0.dist-info → python_bsblan-6.0.0.dist-info}/WHEEL +0 -0
- {python_bsblan-5.2.0.dist-info → python_bsblan-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
bsblan/bsblan.py
CHANGED
|
@@ -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"
|
|
@@ -152,13 +155,10 @@ class BSBLAN:
|
|
|
152
155
|
async def get_available_circuits(self) -> list[int]:
|
|
153
156
|
"""Detect which heating circuits are available on the device.
|
|
154
157
|
|
|
155
|
-
Uses
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
circuit returns ``value="0"`` with ``desc="---"``.
|
|
160
|
-
|
|
161
|
-
A circuit is only considered available when both checks pass.
|
|
158
|
+
Uses the configured operating mode probe parameters from
|
|
159
|
+
CircuitConfig.PROBE_PARAMS as the only discovery signal. Status
|
|
160
|
+
parameters are not queried during discovery to keep setup lightweight
|
|
161
|
+
and avoid excluding valid circuits when status data is unavailable.
|
|
162
162
|
|
|
163
163
|
This is useful for integration setup flows (e.g., Home Assistant
|
|
164
164
|
config flow) to discover how many circuits the user's controller
|
|
@@ -173,54 +173,48 @@ class BSBLAN:
|
|
|
173
173
|
# circuits == [1, 2] for a dual-circuit controller
|
|
174
174
|
|
|
175
175
|
"""
|
|
176
|
+
if self._uses_pps_bus:
|
|
177
|
+
return await self._get_available_pps_circuits()
|
|
178
|
+
|
|
176
179
|
available: list[int] = []
|
|
177
180
|
for circuit, param_id in CircuitConfig.PROBE_PARAMS.items():
|
|
178
181
|
try:
|
|
179
182
|
response = await self._request(
|
|
180
183
|
params={"Parameter": param_id},
|
|
181
184
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# Secondary check: query the status parameter.
|
|
188
|
-
# Inactive circuits either:
|
|
189
|
-
# - return value="0" and desc="---"
|
|
190
|
-
# - return an empty dict {} (param not supported)
|
|
191
|
-
status_id = CircuitConfig.STATUS_PARAMS[circuit]
|
|
192
|
-
status_resp = await self._request(
|
|
193
|
-
params={"Parameter": status_id},
|
|
185
|
+
except BSBLANError:
|
|
186
|
+
logger.debug(
|
|
187
|
+
"Circuit %d not available (operating mode request failed)",
|
|
188
|
+
circuit,
|
|
194
189
|
)
|
|
195
|
-
|
|
190
|
+
continue
|
|
196
191
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"Circuit %d has no status data (not supported)",
|
|
201
|
-
circuit,
|
|
202
|
-
)
|
|
203
|
-
continue
|
|
204
|
-
|
|
205
|
-
# value="0" + desc="---" means inactive
|
|
206
|
-
if (
|
|
207
|
-
status_data.get("desc") == CircuitConfig.INACTIVE_MARKER
|
|
208
|
-
and str(status_data.get("value", "")) == "0"
|
|
209
|
-
):
|
|
210
|
-
logger.debug(
|
|
211
|
-
"Circuit %d has status '---' (inactive)",
|
|
212
|
-
circuit,
|
|
213
|
-
)
|
|
214
|
-
continue
|
|
215
|
-
|
|
216
|
-
available.append(circuit)
|
|
217
|
-
except BSBLANError:
|
|
192
|
+
# A circuit exists if the response contains the operating mode key
|
|
193
|
+
# with actual data (not an empty dict).
|
|
194
|
+
if not response.get(param_id):
|
|
218
195
|
logger.debug(
|
|
219
|
-
"Circuit %d
|
|
196
|
+
"Circuit %d has no operating mode data (not supported)",
|
|
220
197
|
circuit,
|
|
221
198
|
)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
available.append(circuit)
|
|
222
202
|
return sorted(available)
|
|
223
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
|
+
|
|
224
218
|
async def _setup_api_validator(self) -> None:
|
|
225
219
|
"""Set up the API validator without validating sections.
|
|
226
220
|
|
|
@@ -234,9 +228,21 @@ class BSBLAN:
|
|
|
234
228
|
if self._api_data is None:
|
|
235
229
|
self._api_data = self._copy_api_config()
|
|
236
230
|
|
|
231
|
+
self._apply_bus_specific_api_config()
|
|
232
|
+
|
|
237
233
|
# Initialize the API validator (but don't validate sections yet)
|
|
238
234
|
self._api_validator = APIValidator(self._api_data)
|
|
239
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
|
+
|
|
240
246
|
async def _ensure_section_validated(
|
|
241
247
|
self, section: SectionLiteral, include: list[str] | None = None
|
|
242
248
|
) -> None:
|
|
@@ -273,11 +279,26 @@ class BSBLAN:
|
|
|
273
279
|
logger.debug("Lazy loading section: %s", section)
|
|
274
280
|
response_data = await self._validate_api_section(section, include)
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
if response_data and self._should_extract_temperature_unit(
|
|
283
|
+
section, include, response_data
|
|
284
|
+
):
|
|
279
285
|
self._extract_temperature_unit_from_response(response_data)
|
|
280
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
|
+
|
|
281
302
|
async def _ensure_hot_water_group_validated(
|
|
282
303
|
self,
|
|
283
304
|
group_name: str,
|
|
@@ -470,17 +491,16 @@ class BSBLAN:
|
|
|
470
491
|
) -> None:
|
|
471
492
|
"""Extract temperature unit from heating section response data.
|
|
472
493
|
|
|
473
|
-
Gets the unit from
|
|
494
|
+
Gets the unit from the target_temperature parameter, which is always
|
|
474
495
|
present in the heating section.
|
|
475
496
|
|
|
476
497
|
Args:
|
|
477
498
|
response_data: The response data from heating section validation
|
|
478
499
|
|
|
479
500
|
"""
|
|
480
|
-
# Look for
|
|
501
|
+
# Look for target_temperature in the response.
|
|
481
502
|
for param_id, param_data in response_data.items():
|
|
482
|
-
|
|
483
|
-
if param_id == "710" and isinstance(param_data, dict):
|
|
503
|
+
if param_id in {"710", "15004"} and isinstance(param_data, dict):
|
|
484
504
|
unit = param_data.get("unit", "")
|
|
485
505
|
if unit in ("°C", "°C"):
|
|
486
506
|
self._temperature_unit = "°C"
|
|
@@ -489,16 +509,15 @@ class BSBLAN:
|
|
|
489
509
|
else:
|
|
490
510
|
# Keep default if unit is empty or unknown
|
|
491
511
|
logger.debug(
|
|
492
|
-
"Unknown or empty temperature unit from
|
|
493
|
-
"Using default (°C)",
|
|
512
|
+
"Unknown or empty temperature unit from heating target: "
|
|
513
|
+
"'%s'. Using default (°C)",
|
|
494
514
|
unit,
|
|
495
515
|
)
|
|
496
516
|
logger.debug("Temperature unit set to: %s", self._temperature_unit)
|
|
497
517
|
return
|
|
498
518
|
|
|
499
|
-
# If we didn't find parameter 710, log a warning
|
|
500
519
|
logger.warning(
|
|
501
|
-
"Could not find
|
|
520
|
+
"Could not find target temperature in heating section response. "
|
|
502
521
|
"Using default temperature unit (°C)"
|
|
503
522
|
)
|
|
504
523
|
|
|
@@ -520,6 +539,31 @@ class BSBLAN:
|
|
|
520
539
|
logger.debug("BSBLAN version: %s", self._firmware_version)
|
|
521
540
|
self._set_api_version()
|
|
522
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
|
+
|
|
523
567
|
def _set_api_version(self) -> None:
|
|
524
568
|
"""Set the API version based on the firmware version.
|
|
525
569
|
|
|
@@ -598,8 +642,8 @@ class BSBLAN:
|
|
|
598
642
|
Args:
|
|
599
643
|
circuit: The heating circuit number (1 or 2).
|
|
600
644
|
|
|
601
|
-
Note: Temperature unit is extracted during heating section validation
|
|
602
|
-
|
|
645
|
+
Note: Temperature unit is extracted during heating section validation,
|
|
646
|
+
so no extra API call is needed here.
|
|
603
647
|
|
|
604
648
|
"""
|
|
605
649
|
if circuit in self._circuit_temp_initialized:
|
|
@@ -619,10 +663,20 @@ class BSBLAN:
|
|
|
619
663
|
BSBLANInvalidParameterError: If the circuit number is invalid.
|
|
620
664
|
|
|
621
665
|
"""
|
|
622
|
-
if circuit not in CircuitConfig.VALID:
|
|
666
|
+
if circuit not in CircuitConfig.VALID or (self._uses_pps_bus and circuit != 1):
|
|
623
667
|
msg = ErrorMsg.INVALID_CIRCUIT.format(circuit)
|
|
624
668
|
raise BSBLANInvalidParameterError(msg)
|
|
625
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
|
+
|
|
626
680
|
@property
|
|
627
681
|
def get_temperature_unit(self) -> str:
|
|
628
682
|
"""Get the unit of temperature.
|
|
@@ -904,8 +958,26 @@ class BSBLAN:
|
|
|
904
958
|
params = self._extract_params_summary(section_params)
|
|
905
959
|
data = await self._request(params={"Parameter": params["string_par"]})
|
|
906
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)
|
|
907
963
|
return model_class.model_validate(data)
|
|
908
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
|
+
|
|
909
981
|
async def state(
|
|
910
982
|
self,
|
|
911
983
|
include: list[str] | None = None,
|
|
@@ -927,8 +999,9 @@ class BSBLAN:
|
|
|
927
999
|
State: The current state of the BSBLAN device.
|
|
928
1000
|
|
|
929
1001
|
Note:
|
|
930
|
-
|
|
931
|
-
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.
|
|
932
1005
|
|
|
933
1006
|
Example:
|
|
934
1007
|
# Fetch only hvac_mode and current_temperature
|
|
@@ -1000,7 +1073,8 @@ class BSBLAN:
|
|
|
1000
1073
|
|
|
1001
1074
|
"""
|
|
1002
1075
|
device_info = await self._request(base_path="/JI")
|
|
1003
|
-
|
|
1076
|
+
self._device = Device.model_validate(device_info)
|
|
1077
|
+
return self._device
|
|
1004
1078
|
|
|
1005
1079
|
async def info(self, include: list[str] | None = None) -> Info:
|
|
1006
1080
|
"""Get information about the current heating system config.
|
|
@@ -1027,6 +1101,9 @@ class BSBLAN:
|
|
|
1027
1101
|
DeviceTime: The current time information from the BSB-LAN device.
|
|
1028
1102
|
|
|
1029
1103
|
"""
|
|
1104
|
+
await self._ensure_device_metadata()
|
|
1105
|
+
self._validate_time_sync_supported()
|
|
1106
|
+
|
|
1030
1107
|
# Get only parameter 0 for time
|
|
1031
1108
|
data = await self._request(params={"Parameter": "0"})
|
|
1032
1109
|
# Create the data dictionary in the expected format
|
|
@@ -1044,6 +1121,8 @@ class BSBLAN:
|
|
|
1044
1121
|
BSBLANInvalidParameterError: If the time format is invalid.
|
|
1045
1122
|
|
|
1046
1123
|
"""
|
|
1124
|
+
await self._ensure_device_metadata()
|
|
1125
|
+
self._validate_time_sync_supported()
|
|
1047
1126
|
self._validate_time_format(time_value)
|
|
1048
1127
|
state: dict[str, object] = {
|
|
1049
1128
|
"Parameter": "0",
|
|
@@ -1064,7 +1143,9 @@ class BSBLAN:
|
|
|
1064
1143
|
Args:
|
|
1065
1144
|
target_temperature (str | None): The target temperature to set.
|
|
1066
1145
|
hvac_mode (int | None): The HVAC mode to set as raw integer value.
|
|
1067
|
-
|
|
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.
|
|
1068
1149
|
circuit: The heating circuit number (1 or 2). Defaults to 1.
|
|
1069
1150
|
|
|
1070
1151
|
Example:
|
|
@@ -1076,6 +1157,8 @@ class BSBLAN:
|
|
|
1076
1157
|
|
|
1077
1158
|
"""
|
|
1078
1159
|
self._validate_circuit(circuit)
|
|
1160
|
+
if self._uses_pps_bus:
|
|
1161
|
+
self._validate_bus_write_supported()
|
|
1079
1162
|
await self._initialize_temperature_range(circuit)
|
|
1080
1163
|
|
|
1081
1164
|
self._validate_single_parameter(
|
|
@@ -1108,7 +1191,7 @@ class BSBLAN:
|
|
|
1108
1191
|
dict[str, Any]: The prepared state for the thermostat.
|
|
1109
1192
|
|
|
1110
1193
|
"""
|
|
1111
|
-
param_ids =
|
|
1194
|
+
param_ids = self._thermostat_params(circuit)
|
|
1112
1195
|
state: dict[str, Any] = {}
|
|
1113
1196
|
if target_temperature is not None:
|
|
1114
1197
|
await self._validate_target_temperature(
|
|
@@ -1124,15 +1207,24 @@ class BSBLAN:
|
|
|
1124
1207
|
)
|
|
1125
1208
|
if hvac_mode is not None:
|
|
1126
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]
|
|
1127
1213
|
state.update(
|
|
1128
1214
|
{
|
|
1129
1215
|
"Parameter": param_ids["hvac_mode"],
|
|
1130
|
-
"Value":
|
|
1216
|
+
"Value": hvac_value,
|
|
1131
1217
|
"Type": "1",
|
|
1132
1218
|
},
|
|
1133
1219
|
)
|
|
1134
1220
|
return state
|
|
1135
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
|
+
|
|
1136
1228
|
async def _validate_target_temperature(
|
|
1137
1229
|
self,
|
|
1138
1230
|
target_temperature: str,
|
|
@@ -1177,13 +1269,17 @@ class BSBLAN:
|
|
|
1177
1269
|
"""Validate the HVAC mode.
|
|
1178
1270
|
|
|
1179
1271
|
Args:
|
|
1180
|
-
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.
|
|
1181
1274
|
|
|
1182
1275
|
Raises:
|
|
1183
1276
|
BSBLANInvalidParameterError: If the HVAC mode is invalid.
|
|
1184
1277
|
|
|
1185
1278
|
"""
|
|
1186
|
-
|
|
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:
|
|
1187
1283
|
raise BSBLANInvalidParameterError(str(hvac_mode))
|
|
1188
1284
|
|
|
1189
1285
|
def _validate_time_format(self, time_value: str) -> None:
|
bsblan/constants.py
CHANGED
|
@@ -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
|
bsblan/models.py
CHANGED
|
@@ -326,13 +326,13 @@ class EntityInfo(BaseModel, Generic[T]):
|
|
|
326
326
|
unit: str
|
|
327
327
|
desc: str
|
|
328
328
|
value: T | None = None
|
|
329
|
-
data_type: int = Field(
|
|
329
|
+
data_type: int = Field(validation_alias="dataType", default=0)
|
|
330
330
|
error: int = 0
|
|
331
331
|
readonly: int = 0
|
|
332
332
|
readwrite: int = 0
|
|
333
333
|
precision: float | None = None
|
|
334
|
-
data_type_name: str = Field(default="",
|
|
335
|
-
data_type_family: str = Field(default="",
|
|
334
|
+
data_type_name: str = Field(default="", validation_alias="dataType_name")
|
|
335
|
+
data_type_family: str = Field(default="", validation_alias="dataType_family")
|
|
336
336
|
|
|
337
337
|
@model_validator(mode="before")
|
|
338
338
|
@classmethod
|
|
@@ -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):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
bsblan/__init__.py,sha256=YD_edvxHL5ocUE2iLWhdinqpsRYWJkVrcds0b6aKp9U,1288
|
|
2
|
+
bsblan/bsblan.py,sha256=-Xwju6B5RcRUDYoMcU--WSn5Gvrjv3j6itDnYCtVZhU,66563
|
|
3
|
+
bsblan/constants.py,sha256=Ln0a-4cgKnr6pjtBaPnpdir2vWQTIZunSbHb552adLE,23769
|
|
4
|
+
bsblan/exceptions.py,sha256=jL7qohIMmuVTsdWBB_trKPg5Yzim6JxaOT13h6EJPlk,1770
|
|
5
|
+
bsblan/models.py,sha256=O8VGJGf0Kpqwo4r0SH4xRBmjC759hlnu8wMR9MM_I8E,21457
|
|
6
|
+
bsblan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
bsblan/utility.py,sha256=sS0wWJoqvLAHzwaSLIqQEQ-boHsYFLKAHw8KNTSmdX8,5772
|
|
8
|
+
python_bsblan-6.0.0.dist-info/METADATA,sha256=yCAHEUdVrPSjdvpodvhdR3NZUVJz47eGzkMefg3zkBg,8529
|
|
9
|
+
python_bsblan-6.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
python_bsblan-6.0.0.dist-info/licenses/LICENSE.md,sha256=Shv8HPcD1WbZjBPvfb5r3h_cwaPeVaUZMUqU_XQGwGw,1092
|
|
11
|
+
python_bsblan-6.0.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
bsblan/__init__.py,sha256=YD_edvxHL5ocUE2iLWhdinqpsRYWJkVrcds0b6aKp9U,1288
|
|
2
|
-
bsblan/bsblan.py,sha256=PCNForRgeJFHwZRE_mEcvySQxiYLA2JCIzMJu429Fq4,63034
|
|
3
|
-
bsblan/constants.py,sha256=LoMSrEdZj2zohDFxOQ2JT7J4jIH9clXh7sfkNnnjl5A,22864
|
|
4
|
-
bsblan/exceptions.py,sha256=jL7qohIMmuVTsdWBB_trKPg5Yzim6JxaOT13h6EJPlk,1770
|
|
5
|
-
bsblan/models.py,sha256=qEycc2eVkCK2CXWDXBSqtTyM-to6pH7jX6aKvXeH3R8,20705
|
|
6
|
-
bsblan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
bsblan/utility.py,sha256=sS0wWJoqvLAHzwaSLIqQEQ-boHsYFLKAHw8KNTSmdX8,5772
|
|
8
|
-
python_bsblan-5.2.0.dist-info/METADATA,sha256=MjhQBz4cjiRwunT2OH4X-gRFsprNSIpLBMNu7kam24Y,8529
|
|
9
|
-
python_bsblan-5.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
-
python_bsblan-5.2.0.dist-info/licenses/LICENSE.md,sha256=Shv8HPcD1WbZjBPvfb5r3h_cwaPeVaUZMUqU_XQGwGw,1092
|
|
11
|
-
python_bsblan-5.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|