python-bsblan 4.1.0b0__tar.gz → 4.1.0b2__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-4.1.0b0 → python_bsblan-4.1.0b2}/PKG-INFO +1 -1
- python_bsblan-4.1.0b2/examples/speed_test.py +382 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/pyproject.toml +1 -1
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/bsblan.py +36 -8
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/utility.py +13 -2
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_api_validation.py +13 -4
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_hot_water_additional.py +135 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_include_parameter.py +80 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_info.py +3 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.editorconfig +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.gitattributes +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/CODE_OF_CONDUCT.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/CONTRIBUTING.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/copilot-instructions.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/labels.yml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/prompts/add-parameter.prompt.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/prompts/code-review.prompt.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/release-drafter.yml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/renovate.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/skills/bsblan-parameters/SKILL.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/skills/bsblan-testing/SKILL.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/auto-approve-renovate.yml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/codeql.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/labels.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/linting.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/lock.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/pr-labels.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/release-drafter.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/release.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/stale.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/tests.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/workflows/typing.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.gitignore +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.nvmrc +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.pre-commit-config.yaml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.prettierignore +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.yamllint +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/AGENTS.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/CLAUDE.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/LICENSE.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/README.md +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/examples/control.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/examples/discovery.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/examples/profile_init.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/examples/ruff.toml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/package-lock.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/package.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/sonar-project.properties +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/__init__.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/constants.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/exceptions.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/models.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/src/bsblan/py.typed +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/__init__.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/conftest.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/device.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/dict_version.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/hot_water_state.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/info.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/password.txt +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/sensor.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/state.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/static_state.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/thermostat_hvac.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/thermostat_temp.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/fixtures/time.json +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/ruff.toml +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_api_initialization.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_auth.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_backoff_retry.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_bsblan.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_bsblan_edge_cases.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_configuration.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_constants.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_context_manager.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_device.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_dhw_time_switch.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_entity_info.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_hotwater_state.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_initialization.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_read_parameters.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_reset_validation.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_schedule_models.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_sensor.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_set_hot_water_schedule.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_set_hotwater.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_state.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_static_state.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_temperature_unit.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_temperature_validation.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_thermostat.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_time.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_utility.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_utility_additional.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_utility_edge_cases.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/tests/test_version_errors.py +0 -0
- {python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-bsblan
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.0b2
|
|
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
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Test speed comparison for BSB-LAN API calls.
|
|
2
|
+
|
|
3
|
+
Compares different approaches:
|
|
4
|
+
1. Multiple parallel calls (current approach)
|
|
5
|
+
2. Combined read_parameters call
|
|
6
|
+
3. With/without parameter filtering
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# Set environment variables (optional - will use mDNS discovery if not set)
|
|
10
|
+
export BSBLAN_HOST=10.0.2.60
|
|
11
|
+
export BSBLAN_PORT=80
|
|
12
|
+
export BSBLAN_PASSKEY=your_passkey # if needed
|
|
13
|
+
|
|
14
|
+
# Run the test
|
|
15
|
+
python examples/speed_test.py
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import statistics
|
|
22
|
+
import time
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from bsblan import BSBLAN, BSBLANConfig
|
|
27
|
+
from discovery import get_bsblan_host, get_config_from_env
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Awaitable, Callable
|
|
31
|
+
|
|
32
|
+
# Test configuration
|
|
33
|
+
NUM_WARMUP_RUNS = 2
|
|
34
|
+
NUM_TEST_RUNS = 10
|
|
35
|
+
|
|
36
|
+
# Parameters used in different tests
|
|
37
|
+
INFO_PARAMS = ["6224"] # Current firmware version
|
|
38
|
+
STATIC_PARAMS = ["714", "716"] # Min/max temp setpoints
|
|
39
|
+
ALL_PARAMS = INFO_PARAMS + STATIC_PARAMS
|
|
40
|
+
|
|
41
|
+
# Large parameter sets for scalability testing
|
|
42
|
+
# Heating parameters
|
|
43
|
+
HEATING_PARAMS = ["700", "710", "900", "8000", "8740", "8749"]
|
|
44
|
+
|
|
45
|
+
# Sensor parameters
|
|
46
|
+
SENSOR_PARAMS = ["8700", "8740"]
|
|
47
|
+
|
|
48
|
+
# Hot water parameters (a good mix of config and state)
|
|
49
|
+
HOT_WATER_PARAMS = [
|
|
50
|
+
"1600", # operating_mode
|
|
51
|
+
"1601", # eco_mode_selection
|
|
52
|
+
"1610", # nominal_setpoint
|
|
53
|
+
"1614", # nominal_setpoint_max
|
|
54
|
+
"1612", # reduced_setpoint
|
|
55
|
+
"1620", # release
|
|
56
|
+
"1630", # dhw_charging_priority
|
|
57
|
+
"1640", # legionella_function
|
|
58
|
+
"1641", # legionella_function_periodicity
|
|
59
|
+
"1642", # legionella_function_day
|
|
60
|
+
"1644", # legionella_function_time
|
|
61
|
+
"1645", # legionella_function_setpoint
|
|
62
|
+
"8830", # dhw_actual_value_top_temperature
|
|
63
|
+
"8820", # state_dhw_pump
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Combined large set (~20 params)
|
|
67
|
+
LARGE_PARAM_SET = HEATING_PARAMS + SENSOR_PARAMS + HOT_WATER_PARAMS[:8] # ~16 params
|
|
68
|
+
|
|
69
|
+
# Extra large set (~22 params)
|
|
70
|
+
XLARGE_PARAM_SET = HEATING_PARAMS + SENSOR_PARAMS + HOT_WATER_PARAMS # ~22 params
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BenchmarkResult:
|
|
75
|
+
"""Results from a speed benchmark."""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
times: list[float]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def avg(self) -> float:
|
|
82
|
+
"""Average time."""
|
|
83
|
+
return statistics.mean(self.times)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def min(self) -> float:
|
|
87
|
+
"""Minimum time."""
|
|
88
|
+
return min(self.times)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def max(self) -> float:
|
|
92
|
+
"""Maximum time."""
|
|
93
|
+
return max(self.times)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def median(self) -> float:
|
|
97
|
+
"""Median time."""
|
|
98
|
+
return statistics.median(self.times)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def stdev(self) -> float:
|
|
102
|
+
"""Standard deviation."""
|
|
103
|
+
return statistics.stdev(self.times) if len(self.times) > 1 else 0.0
|
|
104
|
+
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
"""Format results as string."""
|
|
107
|
+
return (
|
|
108
|
+
f"{self.name}:\n"
|
|
109
|
+
f" avg={self.avg:.3f}s, median={self.median:.3f}s, "
|
|
110
|
+
f"min={self.min:.3f}s, max={self.max:.3f}s, stdev={self.stdev:.3f}s"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def run_test(
|
|
115
|
+
name: str,
|
|
116
|
+
test_fn: Callable[[], Awaitable[object]],
|
|
117
|
+
num_runs: int = NUM_TEST_RUNS,
|
|
118
|
+
warmup_runs: int = NUM_WARMUP_RUNS,
|
|
119
|
+
) -> BenchmarkResult:
|
|
120
|
+
"""Run a test function multiple times and collect timing stats.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Test name for display.
|
|
124
|
+
test_fn: Async function to test.
|
|
125
|
+
num_runs: Number of timed test runs.
|
|
126
|
+
warmup_runs: Number of warmup runs (not timed).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
BenchmarkResult with timing statistics.
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
# Warmup runs (not counted)
|
|
133
|
+
for _ in range(warmup_runs):
|
|
134
|
+
await test_fn()
|
|
135
|
+
|
|
136
|
+
# Timed runs
|
|
137
|
+
times: list[float] = []
|
|
138
|
+
for i in range(num_runs):
|
|
139
|
+
start = time.perf_counter()
|
|
140
|
+
await test_fn()
|
|
141
|
+
elapsed = time.perf_counter() - start
|
|
142
|
+
times.append(elapsed)
|
|
143
|
+
print(f" Run {i + 1}/{num_runs}: {elapsed:.3f}s")
|
|
144
|
+
|
|
145
|
+
return BenchmarkResult(name=name, times=times)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def bench_3_parallel_calls(bsblan: BSBLAN) -> None:
|
|
149
|
+
"""Benchmark 3 parallel calls (current initialization approach)."""
|
|
150
|
+
await asyncio.gather(
|
|
151
|
+
bsblan.device(),
|
|
152
|
+
bsblan.info(),
|
|
153
|
+
bsblan.static_values(),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def bench_2_parallel_calls(bsblan: BSBLAN) -> None:
|
|
158
|
+
"""Benchmark 2 calls: device + combined read_parameters."""
|
|
159
|
+
await asyncio.gather(
|
|
160
|
+
bsblan.device(),
|
|
161
|
+
bsblan.read_parameters(ALL_PARAMS),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def bench_1_read_params(bsblan: BSBLAN) -> None:
|
|
166
|
+
"""Benchmark single read_parameters call with all params."""
|
|
167
|
+
await bsblan.read_parameters(ALL_PARAMS)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def bench_static_values_filtered(bsblan: BSBLAN) -> None:
|
|
171
|
+
"""Benchmark static_values with include filter."""
|
|
172
|
+
await bsblan.static_values(include=["min_temp"])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def bench_info_filtered(bsblan: BSBLAN) -> None:
|
|
176
|
+
"""Benchmark info() with include filter."""
|
|
177
|
+
await bsblan.info(include=["device_identification"])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def bench_large_params_single_call(bsblan: BSBLAN) -> None:
|
|
181
|
+
"""Benchmark single call with ~16 parameters."""
|
|
182
|
+
await bsblan.read_parameters(LARGE_PARAM_SET)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def bench_xlarge_params_single_call(bsblan: BSBLAN) -> None:
|
|
186
|
+
"""Benchmark single call with ~22 parameters."""
|
|
187
|
+
await bsblan.read_parameters(XLARGE_PARAM_SET)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def bench_large_params_4_parallel_calls(bsblan: BSBLAN) -> None:
|
|
191
|
+
"""Benchmark 4 parallel calls, each with ~4 parameters."""
|
|
192
|
+
chunk_size = len(LARGE_PARAM_SET) // 4
|
|
193
|
+
chunks = [
|
|
194
|
+
LARGE_PARAM_SET[i : i + chunk_size]
|
|
195
|
+
for i in range(0, len(LARGE_PARAM_SET), chunk_size)
|
|
196
|
+
]
|
|
197
|
+
await asyncio.gather(*[bsblan.read_parameters(chunk) for chunk in chunks])
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def bench_xlarge_params_4_parallel_calls(bsblan: BSBLAN) -> None:
|
|
201
|
+
"""Benchmark 4 parallel calls splitting ~22 parameters."""
|
|
202
|
+
chunk_size = len(XLARGE_PARAM_SET) // 4
|
|
203
|
+
chunks = [
|
|
204
|
+
XLARGE_PARAM_SET[i : i + chunk_size]
|
|
205
|
+
for i in range(0, len(XLARGE_PARAM_SET), chunk_size)
|
|
206
|
+
]
|
|
207
|
+
await asyncio.gather(*[bsblan.read_parameters(chunk) for chunk in chunks])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def bench_xlarge_params_2_parallel_calls(bsblan: BSBLAN) -> None:
|
|
211
|
+
"""Benchmark 2 parallel calls splitting ~22 parameters."""
|
|
212
|
+
mid = len(XLARGE_PARAM_SET) // 2
|
|
213
|
+
await asyncio.gather(
|
|
214
|
+
bsblan.read_parameters(XLARGE_PARAM_SET[:mid]),
|
|
215
|
+
bsblan.read_parameters(XLARGE_PARAM_SET[mid:]),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def bench_info_only(bsblan: BSBLAN) -> None:
|
|
220
|
+
"""Benchmark info() call only."""
|
|
221
|
+
await bsblan.info()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def bench_static_values_only(bsblan: BSBLAN) -> None:
|
|
225
|
+
"""Benchmark static_values() call only."""
|
|
226
|
+
await bsblan.static_values()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def run_all_benchmarks(bsblan: BSBLAN) -> list[BenchmarkResult]:
|
|
230
|
+
"""Run all speed benchmarks and return results."""
|
|
231
|
+
results: list[BenchmarkResult] = []
|
|
232
|
+
|
|
233
|
+
# Define basic tests
|
|
234
|
+
basic_tests = [
|
|
235
|
+
(
|
|
236
|
+
"Test 1: 3 parallel calls (device + info + static_values)",
|
|
237
|
+
"3 parallel calls",
|
|
238
|
+
lambda: bench_3_parallel_calls(bsblan),
|
|
239
|
+
),
|
|
240
|
+
(
|
|
241
|
+
"Test 2: 2 parallel calls (device + read_parameters)",
|
|
242
|
+
"2 parallel calls",
|
|
243
|
+
lambda: bench_2_parallel_calls(bsblan),
|
|
244
|
+
),
|
|
245
|
+
(
|
|
246
|
+
"Test 3: Single read_parameters call",
|
|
247
|
+
"1 read_parameters",
|
|
248
|
+
lambda: bench_1_read_params(bsblan),
|
|
249
|
+
),
|
|
250
|
+
(
|
|
251
|
+
"Test 4: static_values with include filter (min_temp only)",
|
|
252
|
+
"static_values (filtered)",
|
|
253
|
+
lambda: bench_static_values_filtered(bsblan),
|
|
254
|
+
),
|
|
255
|
+
(
|
|
256
|
+
"Test 5: info with include filter (device_identification only)",
|
|
257
|
+
"info (filtered)",
|
|
258
|
+
lambda: bench_info_filtered(bsblan),
|
|
259
|
+
),
|
|
260
|
+
(
|
|
261
|
+
"Test 6: static_values without filter (all params)",
|
|
262
|
+
"static_values (all)",
|
|
263
|
+
lambda: bench_static_values_only(bsblan),
|
|
264
|
+
),
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
for desc, name, bench_fn in basic_tests:
|
|
268
|
+
print(f"\n{desc}")
|
|
269
|
+
result = await run_test(name, bench_fn)
|
|
270
|
+
results.append(result)
|
|
271
|
+
|
|
272
|
+
# Scalability tests
|
|
273
|
+
print("\n" + "=" * 60)
|
|
274
|
+
print("SCALABILITY TESTS - Many Parameters")
|
|
275
|
+
print("=" * 60)
|
|
276
|
+
|
|
277
|
+
scalability_tests = [
|
|
278
|
+
(
|
|
279
|
+
f"Test 7: Single call with {len(LARGE_PARAM_SET)} params",
|
|
280
|
+
f"1 call ({len(LARGE_PARAM_SET)} params)",
|
|
281
|
+
lambda: bench_large_params_single_call(bsblan),
|
|
282
|
+
),
|
|
283
|
+
(
|
|
284
|
+
f"Test 8: 4 parallel calls ({len(LARGE_PARAM_SET)} params split)",
|
|
285
|
+
f"4 calls ({len(LARGE_PARAM_SET)} params)",
|
|
286
|
+
lambda: bench_large_params_4_parallel_calls(bsblan),
|
|
287
|
+
),
|
|
288
|
+
(
|
|
289
|
+
f"Test 9: Single call with {len(XLARGE_PARAM_SET)} params",
|
|
290
|
+
f"1 call ({len(XLARGE_PARAM_SET)} params)",
|
|
291
|
+
lambda: bench_xlarge_params_single_call(bsblan),
|
|
292
|
+
),
|
|
293
|
+
(
|
|
294
|
+
f"Test 10: 2 parallel calls ({len(XLARGE_PARAM_SET)} params split)",
|
|
295
|
+
f"2 calls ({len(XLARGE_PARAM_SET)} params)",
|
|
296
|
+
lambda: bench_xlarge_params_2_parallel_calls(bsblan),
|
|
297
|
+
),
|
|
298
|
+
(
|
|
299
|
+
f"Test 11: 4 parallel calls ({len(XLARGE_PARAM_SET)} params split)",
|
|
300
|
+
f"4 calls ({len(XLARGE_PARAM_SET)} params)",
|
|
301
|
+
lambda: bench_xlarge_params_4_parallel_calls(bsblan),
|
|
302
|
+
),
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
for desc, name, bench_fn in scalability_tests:
|
|
306
|
+
print(f"\n{desc}")
|
|
307
|
+
result = await run_test(name, bench_fn)
|
|
308
|
+
results.append(result)
|
|
309
|
+
|
|
310
|
+
return results
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def print_results(results: list[BenchmarkResult]) -> None:
|
|
314
|
+
"""Print test results summary."""
|
|
315
|
+
print("\n" + "=" * 60)
|
|
316
|
+
print("RESULTS SUMMARY")
|
|
317
|
+
print("=" * 60)
|
|
318
|
+
|
|
319
|
+
for r in results:
|
|
320
|
+
print(f"\n{r}")
|
|
321
|
+
|
|
322
|
+
# Compare approaches
|
|
323
|
+
baseline = results[0].avg # 3 parallel calls
|
|
324
|
+
print("\n" + "-" * 60)
|
|
325
|
+
print("COMPARISON (vs 3 parallel calls baseline)")
|
|
326
|
+
print("-" * 60)
|
|
327
|
+
|
|
328
|
+
for r in results[1:]:
|
|
329
|
+
diff = baseline - r.avg
|
|
330
|
+
pct = (diff / baseline) * 100 if baseline > 0 else 0
|
|
331
|
+
faster_slower = "faster" if diff > 0 else "slower"
|
|
332
|
+
print(f"{r.name}: {abs(diff):.3f}s {faster_slower} ({abs(pct):.1f}%)")
|
|
333
|
+
|
|
334
|
+
# Best approach
|
|
335
|
+
best = min(results, key=lambda x: x.avg)
|
|
336
|
+
print(f"\n✓ Best approach: {best.name} (avg: {best.avg:.3f}s)")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def main() -> None:
|
|
340
|
+
"""Run speed comparison tests."""
|
|
341
|
+
print("=" * 60)
|
|
342
|
+
print("BSB-LAN API Speed Comparison Test")
|
|
343
|
+
print("=" * 60)
|
|
344
|
+
|
|
345
|
+
# Get configuration from environment or discovery
|
|
346
|
+
env_config = get_config_from_env()
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
host, port = await get_bsblan_host(discovery_seconds=3.0)
|
|
350
|
+
except RuntimeError:
|
|
351
|
+
print("\nNo BSB-LAN device found!")
|
|
352
|
+
print("Set BSBLAN_HOST environment variable or enable mDNS on your device.")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
print(f"\nConnecting to: {host}:{port}")
|
|
356
|
+
|
|
357
|
+
# Build config - cast values to str for type safety
|
|
358
|
+
passkey = env_config.get("passkey")
|
|
359
|
+
username = env_config.get("username")
|
|
360
|
+
password = env_config.get("password")
|
|
361
|
+
|
|
362
|
+
config = BSBLANConfig(
|
|
363
|
+
host=host,
|
|
364
|
+
port=port,
|
|
365
|
+
passkey=str(passkey) if passkey else None,
|
|
366
|
+
username=str(username) if username else None,
|
|
367
|
+
password=str(password) if password else None,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async with BSBLAN(config) as bsblan:
|
|
371
|
+
await bsblan.initialize()
|
|
372
|
+
print("✓ BSB-LAN client initialized\n")
|
|
373
|
+
|
|
374
|
+
print(f"Running {NUM_TEST_RUNS} test runs with {NUM_WARMUP_RUNS} warmup runs\n")
|
|
375
|
+
print("-" * 60)
|
|
376
|
+
|
|
377
|
+
results = await run_all_benchmarks(bsblan)
|
|
378
|
+
print_results(results)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
asyncio.run(main())
|
|
@@ -171,7 +171,9 @@ class BSBLAN:
|
|
|
171
171
|
# Initialize the API validator (but don't validate sections yet)
|
|
172
172
|
self._api_validator = APIValidator(self._api_data)
|
|
173
173
|
|
|
174
|
-
async def _ensure_section_validated(
|
|
174
|
+
async def _ensure_section_validated(
|
|
175
|
+
self, section: SectionLiteral, include: list[str] | None = None
|
|
176
|
+
) -> None:
|
|
175
177
|
"""Ensure a section is validated before use (lazy loading).
|
|
176
178
|
|
|
177
179
|
This method validates a section on-demand when it's first accessed.
|
|
@@ -182,6 +184,8 @@ class BSBLAN:
|
|
|
182
184
|
|
|
183
185
|
Args:
|
|
184
186
|
section: The section name to validate
|
|
187
|
+
include: Optional list of parameter names to validate. If None,
|
|
188
|
+
validates all parameters for the section.
|
|
185
189
|
|
|
186
190
|
"""
|
|
187
191
|
if not self._api_validator:
|
|
@@ -201,7 +205,7 @@ class BSBLAN:
|
|
|
201
205
|
return
|
|
202
206
|
|
|
203
207
|
logger.debug("Lazy loading section: %s", section)
|
|
204
|
-
response_data = await self._validate_api_section(section)
|
|
208
|
+
response_data = await self._validate_api_section(section, include)
|
|
205
209
|
|
|
206
210
|
# Extract temperature unit from heating section validation
|
|
207
211
|
# (parameter 710 - target_temperature is always in heating section)
|
|
@@ -209,7 +213,10 @@ class BSBLAN:
|
|
|
209
213
|
self._extract_temperature_unit_from_response(response_data)
|
|
210
214
|
|
|
211
215
|
async def _ensure_hot_water_group_validated(
|
|
212
|
-
self,
|
|
216
|
+
self,
|
|
217
|
+
group_name: str,
|
|
218
|
+
param_filter: set[str],
|
|
219
|
+
include: list[str] | None = None,
|
|
213
220
|
) -> None:
|
|
214
221
|
"""Validate only a specific hot water parameter group (lazy loading).
|
|
215
222
|
|
|
@@ -223,6 +230,8 @@ class BSBLAN:
|
|
|
223
230
|
Args:
|
|
224
231
|
group_name: Name of the group (essential, config, schedule)
|
|
225
232
|
param_filter: Set of parameter IDs for this group
|
|
233
|
+
include: Optional list of parameter names to include in validation.
|
|
234
|
+
If provided, only these parameters will be validated.
|
|
226
235
|
|
|
227
236
|
"""
|
|
228
237
|
# Fast path: skip if already validated (no lock needed)
|
|
@@ -256,6 +265,14 @@ class BSBLAN:
|
|
|
256
265
|
if param_id in param_filter
|
|
257
266
|
}
|
|
258
267
|
|
|
268
|
+
# Apply include filter if specified - only validate requested params
|
|
269
|
+
if include is not None:
|
|
270
|
+
group_params = {
|
|
271
|
+
param_id: name
|
|
272
|
+
for param_id, name in group_params.items()
|
|
273
|
+
if name in include
|
|
274
|
+
}
|
|
275
|
+
|
|
259
276
|
if not group_params:
|
|
260
277
|
logger.debug("No parameters to validate for group %s", group_name)
|
|
261
278
|
self._validated_hot_water_groups.add(group_name)
|
|
@@ -337,12 +354,14 @@ class BSBLAN:
|
|
|
337
354
|
self._extract_temperature_unit_from_response(response_data)
|
|
338
355
|
|
|
339
356
|
async def _validate_api_section(
|
|
340
|
-
self, section: SectionLiteral
|
|
357
|
+
self, section: SectionLiteral, include: list[str] | None = None
|
|
341
358
|
) -> dict[str, Any] | None:
|
|
342
359
|
"""Validate a specific section of the API configuration.
|
|
343
360
|
|
|
344
361
|
Args:
|
|
345
362
|
section: The section name to validate
|
|
363
|
+
include: Optional list of parameter names to validate. If None,
|
|
364
|
+
validates all parameters for the section.
|
|
346
365
|
|
|
347
366
|
Returns:
|
|
348
367
|
dict[str, Any] | None: The response data from the device, or None if
|
|
@@ -371,6 +390,14 @@ class BSBLAN:
|
|
|
371
390
|
error_msg = f"Section '{section}' not found in API data"
|
|
372
391
|
raise BSBLANError(error_msg) from err
|
|
373
392
|
|
|
393
|
+
# Filter to only included params if specified
|
|
394
|
+
if include is not None:
|
|
395
|
+
section_data = {
|
|
396
|
+
param_id: name
|
|
397
|
+
for param_id, name in section_data.items()
|
|
398
|
+
if name in include
|
|
399
|
+
}
|
|
400
|
+
|
|
374
401
|
try:
|
|
375
402
|
# Request data from device for validation
|
|
376
403
|
params = await self._extract_params_summary(section_data)
|
|
@@ -379,7 +406,7 @@ class BSBLAN:
|
|
|
379
406
|
)
|
|
380
407
|
|
|
381
408
|
# Validate the section against actual device response
|
|
382
|
-
api_validator.validate_section(section, response_data)
|
|
409
|
+
api_validator.validate_section(section, response_data, include)
|
|
383
410
|
# Update API data with validated configuration
|
|
384
411
|
if self._api_data:
|
|
385
412
|
self._api_data[section] = api_validator.get_section_params(section)
|
|
@@ -794,8 +821,8 @@ class BSBLAN:
|
|
|
794
821
|
are valid for this section.
|
|
795
822
|
|
|
796
823
|
"""
|
|
797
|
-
# Lazy load: validate section on first access
|
|
798
|
-
await self._ensure_section_validated(section)
|
|
824
|
+
# Lazy load: validate section on first access (only for included params)
|
|
825
|
+
await self._ensure_section_validated(section, include)
|
|
799
826
|
|
|
800
827
|
section_params = self._api_validator.get_section_params(section)
|
|
801
828
|
|
|
@@ -1091,7 +1118,8 @@ class BSBLAN:
|
|
|
1091
1118
|
|
|
1092
1119
|
"""
|
|
1093
1120
|
# Granular lazy load: validate only this param group on first access
|
|
1094
|
-
|
|
1121
|
+
# Pass include filter so we only validate requested params
|
|
1122
|
+
await self._ensure_hot_water_group_validated(group_name, param_filter, include)
|
|
1095
1123
|
|
|
1096
1124
|
# Use cached validated params
|
|
1097
1125
|
filtered_params = {
|
|
@@ -77,13 +77,20 @@ class APIValidator:
|
|
|
77
77
|
api_config: Any # intentionally permissive to support tests and dynamic data
|
|
78
78
|
validated_sections: set[str] = field(default_factory=set)
|
|
79
79
|
|
|
80
|
-
def validate_section(
|
|
80
|
+
def validate_section(
|
|
81
|
+
self,
|
|
82
|
+
section: str,
|
|
83
|
+
request_data: dict[str, Any],
|
|
84
|
+
include: list[str] | None = None,
|
|
85
|
+
) -> None:
|
|
81
86
|
"""Validate and update a section of API config based on actual device support.
|
|
82
87
|
|
|
83
88
|
Args:
|
|
84
89
|
section: The section of the API config to validate
|
|
85
90
|
(e.g., 'heating', 'hot_water')
|
|
86
91
|
request_data: Response data from the device for validation
|
|
92
|
+
include: Optional list of parameter names to validate. If None,
|
|
93
|
+
validates all parameters for the section.
|
|
87
94
|
|
|
88
95
|
"""
|
|
89
96
|
# Check if the section exists in the APIConfig object
|
|
@@ -99,8 +106,12 @@ class APIValidator:
|
|
|
99
106
|
section_config = self.api_config[section]
|
|
100
107
|
params_to_remove = []
|
|
101
108
|
|
|
102
|
-
# Check each parameter in the section
|
|
109
|
+
# Check each parameter in the section (filtered by include if specified)
|
|
103
110
|
for param_id, param_name in section_config.items():
|
|
111
|
+
# Skip params not in include list (if include is specified)
|
|
112
|
+
if include is not None and param_name not in include:
|
|
113
|
+
continue
|
|
114
|
+
|
|
104
115
|
if param_id not in request_data:
|
|
105
116
|
logger.info(
|
|
106
117
|
"Parameter %s (%s) not found in device response",
|
|
@@ -163,7 +163,10 @@ async def test_validate_api_section_validation_error(
|
|
|
163
163
|
bsblan._api_validator = APIValidator(bsblan._api_data)
|
|
164
164
|
|
|
165
165
|
def mock_validate(
|
|
166
|
-
_self: APIValidator,
|
|
166
|
+
_self: APIValidator,
|
|
167
|
+
_section: str,
|
|
168
|
+
_response: dict[str, Any],
|
|
169
|
+
_include: list[str] | None = None,
|
|
167
170
|
) -> NoReturn:
|
|
168
171
|
error_message = "Validation error"
|
|
169
172
|
raise BSBLANError(error_message)
|
|
@@ -296,7 +299,9 @@ async def test_ensure_section_validated_double_check_after_lock() -> None:
|
|
|
296
299
|
# Track validation calls
|
|
297
300
|
validation_count = 0
|
|
298
301
|
|
|
299
|
-
async def mock_validate(
|
|
302
|
+
async def mock_validate(
|
|
303
|
+
section: str, _include: list[str] | None = None
|
|
304
|
+
) -> dict[str, Any]:
|
|
300
305
|
nonlocal validation_count
|
|
301
306
|
validation_count += 1
|
|
302
307
|
# Mark section as validated
|
|
@@ -329,7 +334,9 @@ async def test_ensure_section_validated_concurrent_double_check() -> None:
|
|
|
329
334
|
validation_count = 0
|
|
330
335
|
validation_started = asyncio.Event()
|
|
331
336
|
|
|
332
|
-
async def slow_validate(
|
|
337
|
+
async def slow_validate(
|
|
338
|
+
section: str, _include: list[str] | None = None
|
|
339
|
+
) -> dict[str, Any]:
|
|
333
340
|
nonlocal validation_count
|
|
334
341
|
validation_count += 1
|
|
335
342
|
validation_started.set()
|
|
@@ -392,7 +399,9 @@ async def test_ensure_section_validated_heating_extracts_temp_unit() -> None:
|
|
|
392
399
|
bsblan._api_validator = APIValidator(bsblan._api_data)
|
|
393
400
|
|
|
394
401
|
# Mock _validate_api_section to return response with temp unit
|
|
395
|
-
async def mock_validate(
|
|
402
|
+
async def mock_validate(
|
|
403
|
+
section: str, _include: list[str] | None = None
|
|
404
|
+
) -> dict[str, Any]:
|
|
396
405
|
bsblan._api_validator.validated_sections.add(section)
|
|
397
406
|
if section == "heating":
|
|
398
407
|
return {"710": {"value": "20.0", "unit": "°F"}}
|
|
@@ -571,3 +571,138 @@ async def test_ensure_hot_water_group_concurrent_double_check() -> None:
|
|
|
571
571
|
|
|
572
572
|
# Only one request should have been made
|
|
573
573
|
assert request_count == 1
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@pytest.mark.asyncio
|
|
577
|
+
async def test_ensure_hot_water_group_validated_with_include_filter() -> None:
|
|
578
|
+
"""Test that include filter limits which params are validated."""
|
|
579
|
+
async with aiohttp.ClientSession() as session:
|
|
580
|
+
bsblan = BSBLAN(BSBLANConfig(host="example.com"), session=session)
|
|
581
|
+
bsblan._api_version = "v3"
|
|
582
|
+
# Set up api_data with multiple params in the config group
|
|
583
|
+
bsblan._api_data = { # type: ignore[assignment]
|
|
584
|
+
"hot_water": {
|
|
585
|
+
"1640": "legionella_function",
|
|
586
|
+
"1645": "legionella_function_setpoint",
|
|
587
|
+
"1648": "legionella_circulation_temp_diff",
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
bsblan._api_validator = APIValidator(bsblan._api_data)
|
|
591
|
+
|
|
592
|
+
requested_params: list[str] = []
|
|
593
|
+
|
|
594
|
+
async def mock_request(
|
|
595
|
+
params: dict[str, str] | None = None,
|
|
596
|
+
**_kwargs: Any,
|
|
597
|
+
) -> dict[str, Any]:
|
|
598
|
+
if params:
|
|
599
|
+
requested_params.append(params.get("Parameter", ""))
|
|
600
|
+
# Return valid data for all requested params
|
|
601
|
+
return {
|
|
602
|
+
"1640": {"value": "1", "unit": ""},
|
|
603
|
+
"1645": {"value": "60", "unit": "°C"},
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
bsblan._request = mock_request # type: ignore[method-assign]
|
|
607
|
+
|
|
608
|
+
# Validate with include filter - only request 2 of 3 params
|
|
609
|
+
await bsblan._ensure_hot_water_group_validated(
|
|
610
|
+
"config",
|
|
611
|
+
{"1640", "1645", "1648"},
|
|
612
|
+
include=["legionella_function", "legionella_function_setpoint"],
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Verify only filtered params were requested (not 1648)
|
|
616
|
+
assert len(requested_params) == 1
|
|
617
|
+
assert "1648" not in requested_params[0]
|
|
618
|
+
assert "1640" in requested_params[0]
|
|
619
|
+
assert "1645" in requested_params[0]
|
|
620
|
+
|
|
621
|
+
# Cache should only contain the validated params
|
|
622
|
+
assert "1640" in bsblan._hot_water_param_cache
|
|
623
|
+
assert "1645" in bsblan._hot_water_param_cache
|
|
624
|
+
assert "1648" not in bsblan._hot_water_param_cache
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@pytest.mark.asyncio
|
|
628
|
+
async def test_ensure_hot_water_group_validated_include_empty_result() -> None:
|
|
629
|
+
"""Test that include filter with no matching params marks group validated."""
|
|
630
|
+
async with aiohttp.ClientSession() as session:
|
|
631
|
+
bsblan = BSBLAN(BSBLANConfig(host="example.com"), session=session)
|
|
632
|
+
bsblan._api_version = "v3"
|
|
633
|
+
bsblan._api_data = { # type: ignore[assignment]
|
|
634
|
+
"hot_water": {
|
|
635
|
+
"1640": "legionella_function",
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
bsblan._api_validator = APIValidator(bsblan._api_data)
|
|
639
|
+
|
|
640
|
+
request_count = 0
|
|
641
|
+
|
|
642
|
+
async def mock_request(**_kwargs: Any) -> dict[str, Any]:
|
|
643
|
+
nonlocal request_count
|
|
644
|
+
request_count += 1
|
|
645
|
+
return {}
|
|
646
|
+
|
|
647
|
+
bsblan._request = mock_request # type: ignore[method-assign]
|
|
648
|
+
|
|
649
|
+
# Include filter with a param name that doesn't exist in the group
|
|
650
|
+
await bsblan._ensure_hot_water_group_validated(
|
|
651
|
+
"config",
|
|
652
|
+
{"1640"},
|
|
653
|
+
include=["nonexistent_param"],
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# No request should be made since no params match
|
|
657
|
+
assert request_count == 0
|
|
658
|
+
# Group should still be marked as validated
|
|
659
|
+
assert "config" in bsblan._validated_hot_water_groups
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
@pytest.mark.asyncio
|
|
663
|
+
async def test_ensure_hot_water_group_validated_without_include() -> None:
|
|
664
|
+
"""Test that without include filter all group params are validated."""
|
|
665
|
+
async with aiohttp.ClientSession() as session:
|
|
666
|
+
bsblan = BSBLAN(BSBLANConfig(host="example.com"), session=session)
|
|
667
|
+
bsblan._api_version = "v3"
|
|
668
|
+
bsblan._api_data = { # type: ignore[assignment]
|
|
669
|
+
"hot_water": {
|
|
670
|
+
"1640": "legionella_function",
|
|
671
|
+
"1645": "legionella_function_setpoint",
|
|
672
|
+
"1648": "legionella_circulation_temp_diff",
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
bsblan._api_validator = APIValidator(bsblan._api_data)
|
|
676
|
+
|
|
677
|
+
requested_params: list[str] = []
|
|
678
|
+
|
|
679
|
+
async def mock_request(
|
|
680
|
+
params: dict[str, str] | None = None,
|
|
681
|
+
**_kwargs: Any,
|
|
682
|
+
) -> dict[str, Any]:
|
|
683
|
+
if params:
|
|
684
|
+
requested_params.append(params.get("Parameter", ""))
|
|
685
|
+
return {
|
|
686
|
+
"1640": {"value": "1", "unit": ""},
|
|
687
|
+
"1645": {"value": "60", "unit": "°C"},
|
|
688
|
+
"1648": {"value": "5", "unit": "K"},
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
bsblan._request = mock_request # type: ignore[method-assign]
|
|
692
|
+
|
|
693
|
+
# Validate without include filter - all params should be requested
|
|
694
|
+
await bsblan._ensure_hot_water_group_validated(
|
|
695
|
+
"config",
|
|
696
|
+
{"1640", "1645", "1648"},
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# All 3 params should be in the request
|
|
700
|
+
assert len(requested_params) == 1
|
|
701
|
+
assert "1640" in requested_params[0]
|
|
702
|
+
assert "1645" in requested_params[0]
|
|
703
|
+
assert "1648" in requested_params[0]
|
|
704
|
+
|
|
705
|
+
# All params should be cached
|
|
706
|
+
assert "1640" in bsblan._hot_water_param_cache
|
|
707
|
+
assert "1645" in bsblan._hot_water_param_cache
|
|
708
|
+
assert "1648" in bsblan._hot_water_param_cache
|
|
@@ -164,6 +164,86 @@ async def test_section_method_with_invalid_params(
|
|
|
164
164
|
request_mock.assert_not_awaited()
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
# ========== Tests for _validate_api_section with include parameter ==========
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.asyncio
|
|
171
|
+
async def test_validate_api_section_with_include_filter(monkeypatch: Any) -> None:
|
|
172
|
+
"""Test _validate_api_section filters params when include is specified."""
|
|
173
|
+
async with aiohttp.ClientSession() as session:
|
|
174
|
+
config = BSBLANConfig(host="example.com")
|
|
175
|
+
bsblan = BSBLAN(config, session=session)
|
|
176
|
+
|
|
177
|
+
monkeypatch.setattr(bsblan, "_firmware_version", "1.0.38-20200730234859")
|
|
178
|
+
monkeypatch.setattr(bsblan, "_api_version", "v3")
|
|
179
|
+
|
|
180
|
+
# Set up API data with multiple params
|
|
181
|
+
api_data = {
|
|
182
|
+
"heating": {
|
|
183
|
+
"700": "hvac_mode",
|
|
184
|
+
"710": "target_temperature",
|
|
185
|
+
"8740": "current_temperature",
|
|
186
|
+
},
|
|
187
|
+
"sensor": {},
|
|
188
|
+
"staticValues": {},
|
|
189
|
+
"device": {},
|
|
190
|
+
"hot_water": {},
|
|
191
|
+
}
|
|
192
|
+
bsblan._api_data = api_data # type: ignore[assignment]
|
|
193
|
+
|
|
194
|
+
api_validator = APIValidator(api_data)
|
|
195
|
+
bsblan._api_validator = api_validator
|
|
196
|
+
|
|
197
|
+
# Mock request to return only the filtered param
|
|
198
|
+
request_mock = AsyncMock(
|
|
199
|
+
return_value={"700": {"value": "1", "unit": "", "desc": "Auto"}}
|
|
200
|
+
)
|
|
201
|
+
monkeypatch.setattr(bsblan, "_request", request_mock)
|
|
202
|
+
|
|
203
|
+
# Validate with include filter - should only request hvac_mode
|
|
204
|
+
result = await bsblan._validate_api_section("heating", include=["hvac_mode"])
|
|
205
|
+
|
|
206
|
+
# Verify only filtered param was requested
|
|
207
|
+
request_mock.assert_awaited_once()
|
|
208
|
+
call_args = request_mock.call_args
|
|
209
|
+
assert call_args.kwargs["params"]["Parameter"] == "700"
|
|
210
|
+
|
|
211
|
+
# Result should contain the response data
|
|
212
|
+
assert result is not None
|
|
213
|
+
assert "700" in result
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.mark.asyncio
|
|
217
|
+
async def test_validate_section_skips_params_not_in_include() -> None:
|
|
218
|
+
"""Test validate_section skips params not in include list."""
|
|
219
|
+
# Set up API config with multiple params
|
|
220
|
+
api_config = {
|
|
221
|
+
"heating": {
|
|
222
|
+
"700": "hvac_mode",
|
|
223
|
+
"710": "target_temperature",
|
|
224
|
+
"8740": "current_temperature",
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
api_validator = APIValidator(api_config)
|
|
229
|
+
|
|
230
|
+
# Mock request data with only one param (simulating filtered response)
|
|
231
|
+
request_data = {"700": {"value": "1", "unit": "", "desc": "Auto"}}
|
|
232
|
+
|
|
233
|
+
# Validate with include filter - should skip params not in include
|
|
234
|
+
api_validator.validate_section("heating", request_data, include=["hvac_mode"])
|
|
235
|
+
|
|
236
|
+
# Section should be validated
|
|
237
|
+
assert api_validator.is_section_validated("heating")
|
|
238
|
+
|
|
239
|
+
# hvac_mode (700) should still be in config since it was valid
|
|
240
|
+
# Other params should NOT be removed since they weren't in include
|
|
241
|
+
assert "700" in api_validator.api_config["heating"]
|
|
242
|
+
# 710 and 8740 were not validated (skipped), so they remain
|
|
243
|
+
assert "710" in api_validator.api_config["heating"]
|
|
244
|
+
assert "8740" in api_validator.api_config["heating"]
|
|
245
|
+
|
|
246
|
+
|
|
167
247
|
# ========== Additional state() specific tests ==========
|
|
168
248
|
|
|
169
249
|
|
|
@@ -46,6 +46,9 @@ async def test_info(aresponses: ResponsesMockServer, monkeypatch: Any) -> None:
|
|
|
46
46
|
|
|
47
47
|
info: Info = await bsblan.info()
|
|
48
48
|
assert info
|
|
49
|
+
assert info.controller_family is not None
|
|
50
|
+
assert info.controller_variant is not None
|
|
51
|
+
assert info.device_identification is not None
|
|
49
52
|
assert info.controller_family.value == 211
|
|
50
53
|
assert info.controller_variant.value == 127
|
|
51
54
|
assert info.device_identification.value == "RVS21.831F/127"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_bsblan-4.1.0b0 → python_bsblan-4.1.0b2}/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|