Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ ENV/
# ruff
.ruff_cache

# uv
uv.lock

# Visual Studio Code
.vscode

Expand Down
87 changes: 2 additions & 85 deletions examples/speed_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
- basic: Original tests (parallel calls, read_parameters, filtering)
- scalability: Large parameter set tests
- dual-circuit: Single vs parallel calls for dual heating circuit params
- triple-circuit: Same idea extended to 3 heating circuits
- hot-water: Hot water parameter group loading tests

Usage:
Expand Down Expand Up @@ -56,29 +55,20 @@
ALL_PARAMS = INFO_PARAMS + STATIC_PARAMS

# Heating circuit 1 (700-series)
HC1_PARAMS = ["700", "710", "900", "8000", "8740", "8749"]
HC1_PARAMS = ["700", "710", "900", "8000", "8740"]

# Heating circuit 2 (1000-series) — mirrors HC1 with offset
HC2_PARAMS = ["1000", "1010", "1200", "8001", "8741", "8750"]

# Heating circuit 3 (1300-series) — mirrors HC1 with offset
HC3_PARAMS = ["1300", "1310", "1500", "8002", "8742", "8751"]
HC2_PARAMS = ["1000", "1010", "1200", "8001", "8770"]

# Static values per circuit
HC1_STATIC_PARAMS = ["714", "716"]
HC2_STATIC_PARAMS = ["1014", "1016"]
HC3_STATIC_PARAMS = ["1314", "1316"]

# Combined dual circuit parameter sets
DUAL_HEATING_PARAMS = HC1_PARAMS + HC2_PARAMS
DUAL_STATIC_PARAMS = HC1_STATIC_PARAMS + HC2_STATIC_PARAMS
DUAL_ALL_PARAMS = DUAL_HEATING_PARAMS + DUAL_STATIC_PARAMS

# Triple circuit parameter sets
TRIPLE_HEATING_PARAMS = HC1_PARAMS + HC2_PARAMS + HC3_PARAMS
TRIPLE_STATIC_PARAMS = HC1_STATIC_PARAMS + HC2_STATIC_PARAMS + HC3_STATIC_PARAMS
TRIPLE_ALL_PARAMS = TRIPLE_HEATING_PARAMS + TRIPLE_STATIC_PARAMS

# Sensor parameters
SENSOR_PARAMS = ["8700", "8740"]

Expand Down Expand Up @@ -515,78 +505,6 @@ async def _sequential_hc1_hc2() -> None:
return suite


def build_triple_circuit_suite(bsblan: BSBLAN) -> BenchmarkSuite:
"""Build the triple heating circuit benchmark suite.

Same idea as dual-circuit but for 3 circuits. Most systems have
at most 2 circuits; HC3 params will return '---' on those
devices but this still measures the network call overhead.
"""
suite = BenchmarkSuite(
name="Triple Heating Circuit",
description=(
"Compare fetching strategies for 3 heating circuits.\n"
" HC1: " + ", ".join(HC1_PARAMS) + "\n"
" HC2: " + ", ".join(HC2_PARAMS) + "\n"
" HC3: " + ", ".join(HC3_PARAMS)
),
)

suite.add(
(f"HC1+HC2+HC3 combined — 1 call ({len(TRIPLE_HEATING_PARAMS)} params)"),
f"1 call ({len(TRIPLE_HEATING_PARAMS)}p)",
lambda: bsblan.read_parameters(TRIPLE_HEATING_PARAMS),
param_count=len(TRIPLE_HEATING_PARAMS),
)

suite.add(
"HC1+HC2+HC3 parallel — 3 calls",
"3 parallel",
lambda: asyncio.gather(
bsblan.read_parameters(HC1_PARAMS),
bsblan.read_parameters(HC2_PARAMS),
bsblan.read_parameters(HC3_PARAMS),
),
param_count=len(TRIPLE_HEATING_PARAMS),
)

async def _sequential_3() -> None:
await bsblan.read_parameters(HC1_PARAMS)
await bsblan.read_parameters(HC2_PARAMS)
await bsblan.read_parameters(HC3_PARAMS)

suite.add(
"HC1+HC2+HC3 sequential — 3 calls",
"3 sequential",
_sequential_3,
param_count=len(TRIPLE_HEATING_PARAMS),
)

# Full init with static values
suite.add(
(f"All circuits + static — 1 call ({len(TRIPLE_ALL_PARAMS)} params)"),
f"1 call all ({len(TRIPLE_ALL_PARAMS)}p)",
lambda: bsblan.read_parameters(TRIPLE_ALL_PARAMS),
param_count=len(TRIPLE_ALL_PARAMS),
)

suite.add(
"All circuits + static — 6 parallel (heat+static per circ)",
"6 parallel per section",
lambda: asyncio.gather(
bsblan.read_parameters(HC1_PARAMS),
bsblan.read_parameters(HC2_PARAMS),
bsblan.read_parameters(HC3_PARAMS),
bsblan.read_parameters(HC1_STATIC_PARAMS),
bsblan.read_parameters(HC2_STATIC_PARAMS),
bsblan.read_parameters(HC3_STATIC_PARAMS),
),
param_count=len(TRIPLE_ALL_PARAMS),
)

return suite


def build_hot_water_suite(bsblan: BSBLAN) -> BenchmarkSuite:
"""Build the hot water parameter benchmark suite."""
suite = BenchmarkSuite(
Expand Down Expand Up @@ -645,7 +563,6 @@ def build_hot_water_suite(bsblan: BSBLAN) -> BenchmarkSuite:
"basic": build_basic_suite,
"scalability": build_scalability_suite,
"dual-circuit": build_dual_circuit_suite,
"triple-circuit": build_triple_circuit_suite,
"hot-water": build_hot_water_suite,
}

Expand Down
28 changes: 13 additions & 15 deletions src/bsblan/bsblan.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@
"sensor",
"hot_water",
"heating_circuit2",
"heating_circuit3",
"staticValues_circuit2",
"staticValues_circuit3",
]

# TypeVar for hot water data models
Expand Down Expand Up @@ -184,10 +182,10 @@ async def initialize(self) -> None:
async def get_available_circuits(self) -> list[int]:
"""Detect which heating circuits are available on the device.

Uses a two-step probe for each circuit (1, 2, 3):
Uses a two-step probe for each circuit (1, 2):
1. Query the operating mode parameter — the response must be
non-empty and contain actual data.
2. Query the status parameter (8000/8001/8002) — an inactive
2. Query the status parameter (8000/8001) — an inactive
circuit returns ``value="0"`` with ``desc="---"``.

A circuit is only considered available when both checks pass.
Expand Down Expand Up @@ -614,7 +612,7 @@ async def _fetch_temperature_range(
"""Fetch min/max temperature range for a circuit from the device.

Args:
circuit: The heating circuit number (1, 2, or 3).
circuit: The heating circuit number (1 or 2).

Returns:
dict with 'min' and 'max' keys (values may be None if unavailable).
Expand Down Expand Up @@ -661,7 +659,7 @@ async def _initialize_temperature_range(
the staticValues section if not already done.

Args:
circuit: The heating circuit number (1, 2, or 3).
circuit: The heating circuit number (1 or 2).

Note: Temperature unit is extracted during heating section validation
from the response (parameter 710), so no extra API call is needed here.
Expand All @@ -680,7 +678,7 @@ async def _initialize_temperature_range(
self._max_temp = temp_range["max"]
self._temperature_range_initialized = True
else:
# HC2/HC3 use per-circuit storage
# HC2 uses per-circuit storage
self._circuit_temp_ranges[circuit] = temp_range
self._circuit_temp_initialized.add(circuit)

Expand Down Expand Up @@ -1010,9 +1008,9 @@ async def state(
fetches all state parameters. Valid names include:
hvac_mode, target_temperature, hvac_action,
hvac_mode_changeover, current_temperature,
room1_thermostat_mode, room1_temp_setpoint_boost.
circuit: The heating circuit number (1, 2, or 3). Defaults to 1.
Circuit 2 and 3 use separate parameter IDs but return the
room1_temp_setpoint_boost.
circuit: The heating circuit number (1 or 2). Defaults to 1.
Circuit 2 uses separate parameter IDs but returns the
same State model with the same field names.

Returns:
Expand Down Expand Up @@ -1065,7 +1063,7 @@ async def static_values(
include: Optional list of parameter names to fetch. If None,
fetches all static parameters. Valid names include:
min_temp, max_temp.
circuit: The heating circuit number (1, 2, or 3). Defaults to 1.
circuit: The heating circuit number (1 or 2). Defaults to 1.

Returns:
StaticState: The static information from the BSBLAN device.
Expand Down Expand Up @@ -1157,7 +1155,7 @@ async def thermostat(
target_temperature (str | None): The target temperature to set.
hvac_mode (int | None): The HVAC mode to set as raw integer value.
Valid values: 0=off, 1=auto, 2=eco, 3=heat.
circuit: The heating circuit number (1, 2, or 3). Defaults to 1.
circuit: The heating circuit number (1 or 2). Defaults to 1.

Example:
# Set HC1 temperature
Expand Down Expand Up @@ -1194,7 +1192,7 @@ async def _prepare_thermostat_state(
Args:
target_temperature (str | None): The target temperature to set.
hvac_mode (int | None): The HVAC mode to set as raw integer.
circuit: The heating circuit number (1, 2, or 3).
circuit: The heating circuit number (1 or 2).

Returns:
dict[str, Any]: The prepared state for the thermostat.
Expand Down Expand Up @@ -1236,7 +1234,7 @@ async def _validate_target_temperature(

Args:
target_temperature (str): The target temperature to validate.
circuit: The heating circuit number (1, 2, or 3).
circuit: The heating circuit number (1 or 2).

Raises:
BSBLANError: If the temperature range cannot be initialized.
Expand All @@ -1254,7 +1252,7 @@ async def _validate_target_temperature(
min_temp = self._min_temp
max_temp = self._max_temp
else:
# HC2/HC3 use per-circuit storage
# HC2 uses per-circuit storage
if circuit not in self._circuit_temp_initialized:
await self._initialize_temperature_range(circuit)

Expand Down
58 changes: 5 additions & 53 deletions src/bsblan/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

# Supported heating circuits (1-based)
MIN_CIRCUIT: Final[int] = 1
MAX_CIRCUIT: Final[int] = 3
VALID_CIRCUITS: Final[set[int]] = {1, 2, 3}
MAX_CIRCUIT: Final[int] = 2
VALID_CIRCUITS: Final[set[int]] = {1, 2}


# API Versions
Expand All @@ -20,11 +20,9 @@ class APIConfig(TypedDict):
device: dict[str, str]
sensor: dict[str, str]
hot_water: dict[str, str]
# Multi-circuit sections (heating circuit 2 and 3)
# Multi-circuit sections (heating circuit 2)
heating_circuit2: dict[str, str]
heating_circuit3: dict[str, str]
staticValues_circuit2: dict[str, str]
staticValues_circuit3: dict[str, str]


# Base parameters that exist in all API versions
Expand All @@ -35,7 +33,6 @@ class APIConfig(TypedDict):
# -------
"8000": "hvac_action",
"8740": "current_temperature",
"8749": "room1_thermostat_mode",
}

BASE_STATIC_VALUES_PARAMS: Final[dict[str, str]] = {
Expand Down Expand Up @@ -111,8 +108,7 @@ class APIConfig(TypedDict):
"1200": "hvac_mode_changeover",
# -------
"8001": "hvac_action",
"8741": "current_temperature",
"8750": "room1_thermostat_mode",
"8770": "current_temperature",
}

BASE_STATIC_VALUES_CIRCUIT2_PARAMS: Final[dict[str, str]] = {
Expand All @@ -131,68 +127,35 @@ class APIConfig(TypedDict):
"1016": "max_temp",
}

# --- Heating Circuit 3 parameters (1300-series) ---
# These mirror HC1 (700-series) with an offset of +600
BASE_HEATING_CIRCUIT3_PARAMS: Final[dict[str, str]] = {
"1300": "hvac_mode",
"1310": "target_temperature",
"1500": "hvac_mode_changeover",
# -------
"8002": "hvac_action",
"8742": "current_temperature",
"8751": "room1_thermostat_mode",
}

BASE_STATIC_VALUES_CIRCUIT3_PARAMS: Final[dict[str, str]] = {
"1314": "min_temp",
}

V1_STATIC_VALUES_CIRCUIT3_EXTENSIONS: Final[dict[str, str]] = {
"1330": "max_temp",
}

V3_HEATING_CIRCUIT3_EXTENSIONS: Final[dict[str, str]] = {
"1370": "room1_temp_setpoint_boost",
}

V3_STATIC_VALUES_CIRCUIT3_EXTENSIONS: Final[dict[str, str]] = {
"1316": "max_temp",
}

# Mapping from circuit number to section names
CIRCUIT_HEATING_SECTIONS: Final[dict[int, str]] = {
1: "heating",
2: "heating_circuit2",
3: "heating_circuit3",
}

CIRCUIT_STATIC_SECTIONS: Final[dict[int, str]] = {
1: "staticValues",
2: "staticValues_circuit2",
3: "staticValues_circuit3",
}

# Mapping from circuit number to thermostat parameter IDs
CIRCUIT_THERMOSTAT_PARAMS: Final[dict[int, dict[str, str]]] = {
1: {"target_temperature": "710", "hvac_mode": "700"},
2: {"target_temperature": "1010", "hvac_mode": "1000"},
3: {"target_temperature": "1310", "hvac_mode": "1300"},
}

# Parameter IDs used to probe whether a heating circuit exists on the device.
# We query the operating mode (hvac_mode) for each circuit.
CIRCUIT_PROBE_PARAMS: Final[dict[int, str]] = {
1: "700",
2: "1000",
3: "1300",
}

# Status parameter IDs used as a secondary check for circuit availability.
# Inactive circuits return value="0" and desc="---" for these parameters.
CIRCUIT_STATUS_PARAMS: Final[dict[int, str]] = {
1: "8000",
2: "8001",
3: "8002",
}

# Marker value returned by BSB-LAN for parameters on inactive circuits
Expand All @@ -217,30 +180,21 @@ def build_api_config(version: str) -> APIConfig:
"hot_water": BASE_HOT_WATER_PARAMS.copy(),
# Multi-circuit sections
"heating_circuit2": BASE_HEATING_CIRCUIT2_PARAMS.copy(),
"heating_circuit3": BASE_HEATING_CIRCUIT3_PARAMS.copy(),
"staticValues_circuit2": BASE_STATIC_VALUES_CIRCUIT2_PARAMS.copy(),
"staticValues_circuit3": BASE_STATIC_VALUES_CIRCUIT3_PARAMS.copy(),
}

if version == "v1":
config["staticValues"].update(V1_STATIC_VALUES_EXTENSIONS)
config["staticValues_circuit2"].update(
V1_STATIC_VALUES_CIRCUIT2_EXTENSIONS,
)
config["staticValues_circuit3"].update(
V1_STATIC_VALUES_CIRCUIT3_EXTENSIONS,
)
elif version == "v3":
config["heating"].update(V3_HEATING_EXTENSIONS)
config["staticValues"].update(V3_STATIC_VALUES_EXTENSIONS)
config["heating_circuit2"].update(V3_HEATING_CIRCUIT2_EXTENSIONS)
config["staticValues_circuit2"].update(
V3_STATIC_VALUES_CIRCUIT2_EXTENSIONS,
)
config["heating_circuit3"].update(V3_HEATING_CIRCUIT3_EXTENSIONS)
config["staticValues_circuit3"].update(
V3_STATIC_VALUES_CIRCUIT3_EXTENSIONS,
)

return config

Expand Down Expand Up @@ -520,9 +474,7 @@ def get_hvac_action_category(status_code: int) -> HVACActionCategory:
API_DATA_NOT_INITIALIZED_ERROR_MSG: Final[str] = "API data not initialized"
API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG: Final[str] = "API validator not initialized"
SECTION_NOT_FOUND_ERROR_MSG: Final[str] = "Section '{}' not found in API data"
INVALID_CIRCUIT_ERROR_MSG: Final[str] = (
"Invalid circuit number: {}. Must be 1, 2, or 3."
)
INVALID_CIRCUIT_ERROR_MSG: Final[str] = "Invalid circuit number: {}. Must be 1 or 2."
INVALID_RESPONSE_ERROR_MSG: Final[str] = (
"Invalid response format from BSB-LAN device: {}"
)
Expand Down
Loading
Loading