python-bsblan 6.1.7__py3-none-any.whl → 6.1.8__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/_validation.py +104 -51
- bsblan/models.py +7 -2
- bsblan/utility.py +31 -6
- {python_bsblan-6.1.7.dist-info → python_bsblan-6.1.8.dist-info}/METADATA +1 -1
- {python_bsblan-6.1.7.dist-info → python_bsblan-6.1.8.dist-info}/RECORD +7 -7
- {python_bsblan-6.1.7.dist-info → python_bsblan-6.1.8.dist-info}/WHEEL +0 -0
- {python_bsblan-6.1.7.dist-info → python_bsblan-6.1.8.dist-info}/licenses/LICENSE.md +0 -0
bsblan/_validation.py
CHANGED
|
@@ -75,8 +75,9 @@ class SectionValidator:
|
|
|
75
75
|
self._api_validator: APIValidator | None = None
|
|
76
76
|
# Cache of validated hot water parameter id -> name.
|
|
77
77
|
self._hot_water_param_cache: dict[str, str] = {}
|
|
78
|
-
# Track which hot water param groups have been validated.
|
|
78
|
+
# Track which hot water param groups have been fully validated.
|
|
79
79
|
self._validated_hot_water_groups: set[str] = set()
|
|
80
|
+
self._validated_hot_water_parameters: dict[str, set[str]] = {}
|
|
80
81
|
# Locks to prevent concurrent validation of the same section/group.
|
|
81
82
|
self._section_locks: dict[str, asyncio.Lock] = {}
|
|
82
83
|
self._hot_water_group_locks: dict[str, asyncio.Lock] = {}
|
|
@@ -192,6 +193,11 @@ class SectionValidator:
|
|
|
192
193
|
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
|
|
193
194
|
|
|
194
195
|
api_validator = self._api_validator
|
|
196
|
+
if api_validator.is_section_validated(section):
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
def _requested_parameter_ids() -> set[str]:
|
|
200
|
+
return set(self._get_section_params(section, include))
|
|
195
201
|
|
|
196
202
|
async def _validate() -> None:
|
|
197
203
|
logger.debug("Lazy loading section: %s", section)
|
|
@@ -205,7 +211,9 @@ class SectionValidator:
|
|
|
205
211
|
await self._run_once_locked(
|
|
206
212
|
section,
|
|
207
213
|
self._section_locks,
|
|
208
|
-
lambda: api_validator.
|
|
214
|
+
lambda: api_validator.are_parameters_validated(
|
|
215
|
+
section, _requested_parameter_ids()
|
|
216
|
+
),
|
|
209
217
|
_validate,
|
|
210
218
|
)
|
|
211
219
|
|
|
@@ -231,49 +239,38 @@ class SectionValidator:
|
|
|
231
239
|
If provided, only these parameters will be validated.
|
|
232
240
|
|
|
233
241
|
"""
|
|
242
|
+
if group_name in self._validated_hot_water_groups:
|
|
243
|
+
return
|
|
234
244
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
|
|
245
|
+
def _group_params() -> dict[str, str]:
|
|
246
|
+
return self._get_hot_water_group_params(param_filter, include)
|
|
238
247
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
|
|
248
|
+
def _requested_parameter_ids() -> set[str]:
|
|
249
|
+
return set(_group_params())
|
|
242
250
|
|
|
251
|
+
async def _validate() -> None:
|
|
243
252
|
logger.debug("Lazy loading hot water group: %s", group_name)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
param_id: param_name
|
|
251
|
-
for param_id, param_name in section_data.items()
|
|
252
|
-
if param_id in param_filter
|
|
253
|
+
group_params = _group_params()
|
|
254
|
+
uncovered_params = {
|
|
255
|
+
param_id: name
|
|
256
|
+
for param_id, name in group_params.items()
|
|
257
|
+
if param_id
|
|
258
|
+
not in self._validated_hot_water_parameters.get(group_name, set())
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
if include is not None:
|
|
257
|
-
group_params = {
|
|
258
|
-
param_id: name
|
|
259
|
-
for param_id, name in group_params.items()
|
|
260
|
-
if name in include
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if not group_params:
|
|
261
|
+
if not uncovered_params:
|
|
264
262
|
logger.debug("No parameters to validate for group %s", group_name)
|
|
265
|
-
self._validated_hot_water_groups.add(group_name)
|
|
266
263
|
return
|
|
267
264
|
|
|
268
265
|
# Request only these specific parameters from the device
|
|
269
|
-
params = self._extract_params_summary(
|
|
266
|
+
params = self._extract_params_summary(uncovered_params)
|
|
270
267
|
response_data = await self._request(
|
|
271
268
|
params={"Parameter": params["string_par"]}
|
|
272
269
|
)
|
|
273
270
|
|
|
274
271
|
# Validate and filter out unsupported params
|
|
275
272
|
params_to_remove = []
|
|
276
|
-
for param_id, param_name in
|
|
273
|
+
for param_id, param_name in uncovered_params.items():
|
|
277
274
|
if param_id not in response_data:
|
|
278
275
|
logger.info(
|
|
279
276
|
"Hot water param %s (%s) not found in response",
|
|
@@ -294,26 +291,87 @@ class SectionValidator:
|
|
|
294
291
|
params_to_remove.append(param_id)
|
|
295
292
|
|
|
296
293
|
# Update the cache with validated params for this group
|
|
297
|
-
for param_id, param_name in
|
|
294
|
+
for param_id, param_name in uncovered_params.items():
|
|
298
295
|
if param_id not in params_to_remove:
|
|
299
296
|
self._hot_water_param_cache[param_id] = param_name
|
|
300
297
|
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
validated_ids = self._validated_hot_water_parameters.setdefault(
|
|
299
|
+
group_name, set()
|
|
300
|
+
)
|
|
301
|
+
validated_ids.update(uncovered_params)
|
|
302
|
+
if (
|
|
303
|
+
self._get_hot_water_group_params(param_filter, None).keys()
|
|
304
|
+
<= validated_ids
|
|
305
|
+
):
|
|
306
|
+
self._validated_hot_water_groups.add(group_name)
|
|
307
|
+
else:
|
|
308
|
+
self._validated_hot_water_groups.discard(group_name)
|
|
303
309
|
logger.debug(
|
|
304
310
|
"Validated hot water group '%s': %d params, removed %d unsupported",
|
|
305
311
|
group_name,
|
|
306
|
-
len(
|
|
312
|
+
len(uncovered_params),
|
|
307
313
|
len(params_to_remove),
|
|
308
314
|
)
|
|
309
315
|
|
|
310
316
|
await self._run_once_locked(
|
|
311
317
|
group_name,
|
|
312
318
|
self._hot_water_group_locks,
|
|
313
|
-
lambda:
|
|
319
|
+
lambda: self._are_hot_water_parameters_validated(
|
|
320
|
+
group_name, _requested_parameter_ids()
|
|
321
|
+
),
|
|
314
322
|
_validate,
|
|
315
323
|
)
|
|
316
324
|
|
|
325
|
+
def _get_section_params(
|
|
326
|
+
self, section: SectionLiteral, include: list[str] | None
|
|
327
|
+
) -> dict[str, str]:
|
|
328
|
+
"""Get the configured parameters selected for a section request."""
|
|
329
|
+
api_data = self._get_api_data()
|
|
330
|
+
if not api_data:
|
|
331
|
+
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
section_data = api_data[section]
|
|
335
|
+
except KeyError as err:
|
|
336
|
+
msg = ErrorMsg.SECTION_NOT_FOUND.format(section)
|
|
337
|
+
raise BSBLANError(msg) from err
|
|
338
|
+
|
|
339
|
+
if include is None:
|
|
340
|
+
return section_data
|
|
341
|
+
return {
|
|
342
|
+
param_id: name for param_id, name in section_data.items() if name in include
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def _get_hot_water_group_params(
|
|
346
|
+
self, param_filter: set[str], include: list[str] | None
|
|
347
|
+
) -> dict[str, str]:
|
|
348
|
+
"""Get configured parameters selected for a hot water group request."""
|
|
349
|
+
if not self._api_validator:
|
|
350
|
+
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
|
|
351
|
+
|
|
352
|
+
api_data = self._get_api_data()
|
|
353
|
+
if not api_data:
|
|
354
|
+
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
|
|
355
|
+
|
|
356
|
+
group_params = {
|
|
357
|
+
param_id: param_name
|
|
358
|
+
for param_id, param_name in api_data.get("hot_water", {}).items()
|
|
359
|
+
if param_id in param_filter
|
|
360
|
+
}
|
|
361
|
+
if include is None:
|
|
362
|
+
return group_params
|
|
363
|
+
return {
|
|
364
|
+
param_id: name for param_id, name in group_params.items() if name in include
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
def _are_hot_water_parameters_validated(
|
|
368
|
+
self, group_name: str, parameter_ids: set[str]
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Check whether every requested hot water parameter ID was validated."""
|
|
371
|
+
return group_name in self._validated_hot_water_groups or parameter_ids.issubset(
|
|
372
|
+
self._validated_hot_water_parameters.get(group_name, set())
|
|
373
|
+
)
|
|
374
|
+
|
|
317
375
|
async def _validate_api_section(
|
|
318
376
|
self, section: SectionLiteral, include: list[str] | None = None
|
|
319
377
|
) -> dict[str, Any] | None:
|
|
@@ -340,36 +398,31 @@ class SectionValidator:
|
|
|
340
398
|
if not api_data:
|
|
341
399
|
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
|
|
342
400
|
|
|
343
|
-
# Assign to local variable after asserting it's not None
|
|
344
401
|
api_validator = self._api_validator
|
|
345
|
-
|
|
346
402
|
if api_validator.is_section_validated(section):
|
|
347
403
|
return None
|
|
348
404
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
405
|
+
section_data = self._get_section_params(section, include)
|
|
406
|
+
uncovered_params = {
|
|
407
|
+
param_id: name
|
|
408
|
+
for param_id, name in section_data.items()
|
|
409
|
+
if not api_validator.are_parameters_validated(section, {param_id})
|
|
410
|
+
}
|
|
355
411
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
section_data = {
|
|
359
|
-
param_id: name
|
|
360
|
-
for param_id, name in section_data.items()
|
|
361
|
-
if name in include
|
|
362
|
-
}
|
|
412
|
+
if not uncovered_params:
|
|
413
|
+
return None
|
|
363
414
|
|
|
364
415
|
try:
|
|
365
416
|
# Request data from device for validation
|
|
366
|
-
params = self._extract_params_summary(
|
|
417
|
+
params = self._extract_params_summary(uncovered_params)
|
|
367
418
|
response_data = await self._request(
|
|
368
419
|
params={"Parameter": params["string_par"]}
|
|
369
420
|
)
|
|
370
421
|
|
|
371
422
|
# Validate the section against actual device response
|
|
372
|
-
api_validator.validate_section(
|
|
423
|
+
api_validator.validate_section(
|
|
424
|
+
section, response_data, parameter_ids=set(uncovered_params)
|
|
425
|
+
)
|
|
373
426
|
# Update API data with validated configuration
|
|
374
427
|
api_data[section] = api_validator.get_section_params(section)
|
|
375
428
|
|
bsblan/models.py
CHANGED
|
@@ -254,7 +254,11 @@ def _convert_bsblan_value(
|
|
|
254
254
|
The converted value, or None if the value is inactive.
|
|
255
255
|
|
|
256
256
|
"""
|
|
257
|
-
|
|
257
|
+
# BSB-LAN's `replaceDisabled` setting decides what the firmware sends for a
|
|
258
|
+
# deactivated/inactive numeric parameter. It defaults to "---", but its own
|
|
259
|
+
# BSB_LAN_config.h documents "None" as the value Home Assistant users should
|
|
260
|
+
# pick, so both spellings reach us in the wild.
|
|
261
|
+
if raw_value is None or raw_value in {"---", "", "None"}:
|
|
258
262
|
return None
|
|
259
263
|
|
|
260
264
|
raw = str(raw_value)
|
|
@@ -302,7 +306,8 @@ class EntityInfo(BaseModel, Generic[T]):
|
|
|
302
306
|
- ``EntityInfo[time]`` for HH:MM time values
|
|
303
307
|
- ``EntityInfo[str]`` for string / datetime values
|
|
304
308
|
|
|
305
|
-
When the device returns ``"---"`` (sensor not in use
|
|
309
|
+
When the device returns ``"---"`` or ``"None"`` (sensor not in use, per
|
|
310
|
+
BSB-LAN's ``replaceDisabled`` setting), ``value`` is set
|
|
306
311
|
to ``None``.
|
|
307
312
|
|
|
308
313
|
Attributes:
|
bsblan/utility.py
CHANGED
|
@@ -94,12 +94,14 @@ class APIValidator:
|
|
|
94
94
|
# Flexible type for API data (accepts `APIConfig`, plain dicts, or None)
|
|
95
95
|
api_config: Any # intentionally permissive to support tests and dynamic data
|
|
96
96
|
validated_sections: set[str] = field(default_factory=set)
|
|
97
|
+
validated_parameters: dict[str, set[str]] = field(default_factory=dict)
|
|
97
98
|
|
|
98
99
|
def validate_section(
|
|
99
100
|
self,
|
|
100
101
|
section: str,
|
|
101
102
|
request_data: dict[str, Any],
|
|
102
103
|
include: list[str] | None = None,
|
|
104
|
+
parameter_ids: set[str] | None = None,
|
|
103
105
|
) -> None:
|
|
104
106
|
"""Validate and update a section of API config based on actual device support.
|
|
105
107
|
|
|
@@ -109,6 +111,8 @@ class APIValidator:
|
|
|
109
111
|
request_data: Response data from the device for validation
|
|
110
112
|
include: Optional list of parameter names to validate. If None,
|
|
111
113
|
validates all parameters for the section.
|
|
114
|
+
parameter_ids: Optional parameter IDs to validate. When supplied,
|
|
115
|
+
takes precedence over ``include``.
|
|
112
116
|
|
|
113
117
|
"""
|
|
114
118
|
# Check if the section exists in the APIConfig object
|
|
@@ -116,19 +120,27 @@ class APIValidator:
|
|
|
116
120
|
logger.warning("Unknown section '%s' in API configuration", section)
|
|
117
121
|
return
|
|
118
122
|
|
|
119
|
-
# Skip if section was already validated
|
|
120
123
|
if section in self.validated_sections:
|
|
121
124
|
logger.debug("Section '%s' was already validated", section)
|
|
122
125
|
return
|
|
123
126
|
|
|
124
127
|
section_config = self.api_config[section]
|
|
125
128
|
params_to_remove = []
|
|
129
|
+
validated_ids = set()
|
|
126
130
|
|
|
127
131
|
# Check each parameter in the section (filtered by include if specified)
|
|
128
|
-
for param_id, param_name in section_config.items():
|
|
132
|
+
for param_id, param_name in list(section_config.items()):
|
|
129
133
|
# Skip params not in include list (if include is specified)
|
|
130
|
-
if
|
|
134
|
+
if parameter_ids is not None and param_id not in parameter_ids:
|
|
131
135
|
continue
|
|
136
|
+
if (
|
|
137
|
+
parameter_ids is None
|
|
138
|
+
and include is not None
|
|
139
|
+
and param_name not in include
|
|
140
|
+
):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
validated_ids.add(param_id)
|
|
132
144
|
|
|
133
145
|
if param_id not in request_data:
|
|
134
146
|
logger.info(
|
|
@@ -153,8 +165,11 @@ class APIValidator:
|
|
|
153
165
|
for param_id in params_to_remove:
|
|
154
166
|
section_config.pop(param_id)
|
|
155
167
|
|
|
156
|
-
|
|
157
|
-
self.
|
|
168
|
+
self.validated_parameters.setdefault(section, set()).update(validated_ids)
|
|
169
|
+
if set(section_config).issubset(self.validated_parameters[section]):
|
|
170
|
+
self.validated_sections.add(section)
|
|
171
|
+
else:
|
|
172
|
+
self.validated_sections.discard(section)
|
|
158
173
|
|
|
159
174
|
logger.debug(
|
|
160
175
|
"Validated section '%s': removed %d unsupported parameters",
|
|
@@ -170,8 +185,14 @@ class APIValidator:
|
|
|
170
185
|
"""Get the parameter mapping for a section."""
|
|
171
186
|
return (self.api_config or {}).get(section, {}).copy()
|
|
172
187
|
|
|
188
|
+
def are_parameters_validated(self, section: str, parameter_ids: set[str]) -> bool:
|
|
189
|
+
"""Check whether every requested parameter ID was validated."""
|
|
190
|
+
return section in self.validated_sections or parameter_ids.issubset(
|
|
191
|
+
self.validated_parameters.get(section, set())
|
|
192
|
+
)
|
|
193
|
+
|
|
173
194
|
def is_section_validated(self, section: str) -> bool:
|
|
174
|
-
"""Check
|
|
195
|
+
"""Check whether every current parameter in a section was validated."""
|
|
175
196
|
return section in self.validated_sections
|
|
176
197
|
|
|
177
198
|
def reset_validation(self, section: str | None = None) -> None:
|
|
@@ -183,5 +204,9 @@ class APIValidator:
|
|
|
183
204
|
"""
|
|
184
205
|
if section is None:
|
|
185
206
|
self.validated_sections.clear()
|
|
207
|
+
self.validated_parameters.clear()
|
|
186
208
|
elif section in self.validated_sections:
|
|
187
209
|
self.validated_sections.remove(section)
|
|
210
|
+
self.validated_parameters.pop(section, None)
|
|
211
|
+
else:
|
|
212
|
+
self.validated_parameters.pop(section, None)
|
|
@@ -5,15 +5,15 @@ bsblan/_schedules.py,sha256=Pb0TrhoZM3Wlf83zZOo2d31xDLI0178TNWRqhljJJmU,5273
|
|
|
5
5
|
bsblan/_temperature.py,sha256=DsxFd6f1EMYKHcrf74ScQk6YqaYuUy0zBPHhiP-C0nY,11366
|
|
6
6
|
bsblan/_thermostat.py,sha256=_Dgvt1iVLGJT2H5i4z9R1RUU44NZCJJwnP1D9rSXDHk,5052
|
|
7
7
|
bsblan/_transport.py,sha256=qNtV_8uJrfYfHlbaExPln4jCaJxqE5sypq5OHyxG8KE,8266
|
|
8
|
-
bsblan/_validation.py,sha256=
|
|
8
|
+
bsblan/_validation.py,sha256=JXXRRo95k9yIaErrHxui30e5b3CXJkRWvxo9Fy47Pgo,17742
|
|
9
9
|
bsblan/_version.py,sha256=hpeIg9JUswHSorY3iSUERcMC1MzwM9Rpdo1wqeh5rc8,2799
|
|
10
10
|
bsblan/bsblan.py,sha256=su_WvelM4FauTyn1RNTJYQZwfwXhSG0Mj6r7RIGNVIg,46887
|
|
11
11
|
bsblan/constants.py,sha256=G0VEImKfc30W68c8RCH3AWRitCTMSVVgXwdhqHOQr7o,25589
|
|
12
12
|
bsblan/exceptions.py,sha256=Z9pXtVc8zhy9FTBkT8leJ1UE9zdOdLHF7pE7C4GjBmg,2795
|
|
13
|
-
bsblan/models.py,sha256=
|
|
13
|
+
bsblan/models.py,sha256=nbQjmkLEdM_MtazTTDS1KISZs9h1iestoZg8byVB9kM,22932
|
|
14
14
|
bsblan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
bsblan/utility.py,sha256=
|
|
16
|
-
python_bsblan-6.1.
|
|
17
|
-
python_bsblan-6.1.
|
|
18
|
-
python_bsblan-6.1.
|
|
19
|
-
python_bsblan-6.1.
|
|
15
|
+
bsblan/utility.py,sha256=5oD3f84ke_r7pTQN2jjhhgtVjLjq1Ohld3LlHN7m8xg,7421
|
|
16
|
+
python_bsblan-6.1.8.dist-info/METADATA,sha256=t4usBriYaK4FCTLbD_YI1zMN9Wye-Mfnylmd21mC-hQ,9585
|
|
17
|
+
python_bsblan-6.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
18
|
+
python_bsblan-6.1.8.dist-info/licenses/LICENSE.md,sha256=Shv8HPcD1WbZjBPvfb5r3h_cwaPeVaUZMUqU_XQGwGw,1092
|
|
19
|
+
python_bsblan-6.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|