From c2553b42d343964a9f183951761e3830076fae1e Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:27:39 +0100 Subject: [PATCH 01/10] refactor: remove support for heating circuit 3 and update related parameters --- src/bsblan/bsblan.py | 10 +++----- src/bsblan/constants.py | 54 ++++------------------------------------- 2 files changed, 9 insertions(+), 55 deletions(-) diff --git a/src/bsblan/bsblan.py b/src/bsblan/bsblan.py index dd864e5c..acb86482 100644 --- a/src/bsblan/bsblan.py +++ b/src/bsblan/bsblan.py @@ -91,9 +91,7 @@ "sensor", "hot_water", "heating_circuit2", - "heating_circuit3", "staticValues_circuit2", - "staticValues_circuit3", ] # TypeVar for hot water data models @@ -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. @@ -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) @@ -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) diff --git a/src/bsblan/constants.py b/src/bsblan/constants.py index 8f7d9136..10463edd 100644 --- a/src/bsblan/constants.py +++ b/src/bsblan/constants.py @@ -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 @@ -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 @@ -111,8 +109,8 @@ class APIConfig(TypedDict): "1200": "hvac_mode_changeover", # ------- "8001": "hvac_action", - "8741": "current_temperature", - "8750": "room1_thermostat_mode", + "8770": "current_temperature", + "8779": "room1_thermostat_mode", } BASE_STATIC_VALUES_CIRCUIT2_PARAMS: Final[dict[str, str]] = { @@ -131,52 +129,21 @@ 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. @@ -184,7 +151,6 @@ class APIConfig(TypedDict): CIRCUIT_PROBE_PARAMS: Final[dict[int, str]] = { 1: "700", 2: "1000", - 3: "1300", } # Status parameter IDs used as a secondary check for circuit availability. @@ -192,7 +158,6 @@ class APIConfig(TypedDict): CIRCUIT_STATUS_PARAMS: Final[dict[int, str]] = { 1: "8000", 2: "8001", - 3: "8002", } # Marker value returned by BSB-LAN for parameters on inactive circuits @@ -217,9 +182,7 @@ 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": @@ -227,9 +190,6 @@ def build_api_config(version: str) -> APIConfig: 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) @@ -237,10 +197,6 @@ def build_api_config(version: str) -> APIConfig: 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 From 34088ba723b4c14de830f5a22953f95fa3d2be6e Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:27:50 +0100 Subject: [PATCH 02/10] refactor: remove support for heating circuit 3 and update related tests --- tests/fixtures/state_circuit2.json | 6 +- tests/test_api_validation.py | 2 - tests/test_circuit.py | 122 +++-------------------------- tests/test_constants.py | 2 - tests/test_hotwater_state.py | 2 - tests/test_initialization.py | 2 - 6 files changed, 15 insertions(+), 121 deletions(-) diff --git a/tests/fixtures/state_circuit2.json b/tests/fixtures/state_circuit2.json index bb343f67..cad2444c 100644 --- a/tests/fixtures/state_circuit2.json +++ b/tests/fixtures/state_circuit2.json @@ -48,8 +48,8 @@ "readwrite": 1, "unit": "" }, - "8741": { - "name": "Room temperature setpoint 1", + "8770": { + "name": "Room temperature actual value 2", "dataType_name": "TEMP", "dataType_family": "VALS", "error": 0, @@ -61,7 +61,7 @@ "readwrite": 1, "unit": "°C" }, - "8750": { + "8779": { "name": "Room thermostat heating circuit 2", "dataType_name": "ENUM", "dataType_family": "ENUM", diff --git a/tests/test_api_validation.py b/tests/test_api_validation.py index b4c4a55e..8c65e739 100644 --- a/tests/test_api_validation.py +++ b/tests/test_api_validation.py @@ -372,9 +372,7 @@ async def test_validate_api_section_hot_water_cache() -> None: "device": {}, "hot_water": {"1600": "operating_mode", "1610": "nominal_setpoint"}, "heating_circuit2": {}, - "heating_circuit3": {}, "staticValues_circuit2": {}, - "staticValues_circuit3": {}, } bsblan._api_validator = APIValidator(bsblan._api_data) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index adc6d21f..e43c8750 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -1,4 +1,4 @@ -"""Tests for multi-circuit (HC1/HC2/HC3) heating support.""" +"""Tests for multi-circuit (HC1/HC2) heating support.""" # pylint: disable=protected-access @@ -43,10 +43,8 @@ async def mock_bsblan_circuit() -> AsyncGenerator[BSBLAN, None]: api_validator = APIValidator(bsblan._api_data) api_validator.validated_sections.add("heating") api_validator.validated_sections.add("heating_circuit2") - api_validator.validated_sections.add("heating_circuit3") api_validator.validated_sections.add("staticValues") api_validator.validated_sections.add("staticValues_circuit2") - api_validator.validated_sections.add("staticValues_circuit3") bsblan._api_validator = api_validator yield bsblan @@ -292,61 +290,6 @@ async def test_thermostat_circuit2_hvac_mode( await mock_bsblan_circuit.thermostat(hvac_mode=1, circuit=2) -@pytest.mark.asyncio -async def test_thermostat_circuit3_temperature( - mock_bsblan_circuit: BSBLAN, - aresponses: ResponsesMockServer, -) -> None: - """Test setting temperature on circuit 3.""" - mock_bsblan_circuit._circuit_temp_ranges[3] = { - "min": 8.0, - "max": 35.0, - } - mock_bsblan_circuit._circuit_temp_initialized.add(3) - - expected_data = { - "Parameter": "1310", - "Value": "25", - "Type": "1", - } - aresponses.add( - "example.com", - "/JS", - "POST", - create_response_handler(expected_data), - ) - await mock_bsblan_circuit.thermostat( - target_temperature="25", - circuit=3, - ) - - -@pytest.mark.asyncio -async def test_thermostat_circuit3_hvac_mode( - mock_bsblan_circuit: BSBLAN, - aresponses: ResponsesMockServer, -) -> None: - """Test setting HVAC mode on circuit 3.""" - mock_bsblan_circuit._circuit_temp_ranges[3] = { - "min": 8.0, - "max": 35.0, - } - mock_bsblan_circuit._circuit_temp_initialized.add(3) - - expected_data = { - "Parameter": "1300", - "Value": "2", - "Type": "1", - } - aresponses.add( - "example.com", - "/JS", - "POST", - create_response_handler(expected_data), - ) - await mock_bsblan_circuit.thermostat(hvac_mode=2, circuit=3) - - @pytest.mark.asyncio async def test_thermostat_circuit1_still_works( mock_bsblan_circuit: BSBLAN, @@ -416,7 +359,7 @@ async def test_invalid_circuit_number( await mock_bsblan_circuit.state(circuit=0) with pytest.raises(BSBLANInvalidParameterError, match="Invalid circuit"): - await mock_bsblan_circuit.state(circuit=4) + await mock_bsblan_circuit.state(circuit=3) with pytest.raises(BSBLANInvalidParameterError, match="Invalid circuit"): await mock_bsblan_circuit.static_values(circuit=99) @@ -613,9 +556,6 @@ async def mock_request( # HC2 status - active if param_id == "8001": return {"8001": {"value": "114", "desc": "Heating mode Comfort"}} - # HC3 operating mode - returns empty (not available) - if param_id == "1300": - return {"1300": {}} return {} bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] @@ -624,38 +564,6 @@ async def mock_request( assert circuits == [1, 2] -@pytest.mark.asyncio -async def test_get_available_circuits_all_three( - mock_bsblan_circuit: BSBLAN, -) -> None: - """Test detecting all three available heating circuits.""" - bsblan = mock_bsblan_circuit - - status_map = { - "8000": {"value": "114", "desc": "Heating mode Comfort"}, - "8001": {"value": "140", "desc": "Heating Reduced"}, - "8002": {"value": "114", "desc": "Heating mode Comfort"}, - } - - async def mock_request( - **kwargs: Any, - ) -> dict[str, Any]: - params = kwargs.get("params", {}) - param_id = params.get("Parameter", "") - # Operating mode params - if param_id in {"700", "1000", "1300"}: - return {param_id: {"value": "1", "unit": "", "desc": "Automatic"}} - # Status params - all active - if param_id in status_map: - return {param_id: status_map[param_id]} - return {} - - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] - - circuits = await bsblan.get_available_circuits() - assert circuits == [1, 2, 3] - - @pytest.mark.asyncio async def test_get_available_circuits_only_one( mock_bsblan_circuit: BSBLAN, @@ -677,8 +585,8 @@ async def mock_request( "desc": "Heating mode Comfort", } } - # HC2 and HC3 operating mode - return empty - if param_id in {"1000", "1300"}: + # HC2 operating mode - return empty + if param_id == "1000": return {param_id: {}} return {} @@ -695,7 +603,7 @@ async def test_get_available_circuits_inactive_by_status( """Test that circuits with status '---' are detected as inactive. This is the real-world scenario: the device returns a valid operating - mode for all 3 circuits, but status param shows '---' for HC2/HC3. + mode for all circuits, but status param shows '---' for HC2. """ bsblan = mock_bsblan_circuit @@ -705,7 +613,7 @@ async def mock_request( params = kwargs.get("params", {}) param_id = params.get("Parameter", "") # All circuits return valid operating mode - if param_id in {"700", "1000", "1300"}: + if param_id in {"700", "1000"}: return {param_id: {"value": "1", "unit": "", "desc": "Automatic"}} # HC1 status - active if param_id == "8000": @@ -718,9 +626,6 @@ async def mock_request( # HC2 status - inactive (value=0, desc=---) if param_id == "8001": return {"8001": {"value": "0", "desc": "---"}} - # HC3 status - inactive (value=0, desc=---) - if param_id == "8002": - return {"8002": {"value": "0", "desc": "---"}} return {} bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] @@ -736,7 +641,7 @@ async def test_get_available_circuits_inactive_empty_status( """Test that circuits with empty status response are inactive. Some controllers return an empty dict for status params of circuits - that don't exist (e.g., HC3 status 8002 returns {}). + that don't exist (e.g., HC2 status 8001 returns {}). """ bsblan = mock_bsblan_circuit @@ -746,7 +651,7 @@ async def mock_request( params = kwargs.get("params", {}) param_id = params.get("Parameter", "") # All circuits return valid operating mode - if param_id in {"700", "1000", "1300"}: + if param_id in {"700", "1000"}: return {param_id: {"value": "1", "unit": "", "desc": "Automatic"}} # HC1 status - active if param_id == "8000": @@ -756,11 +661,8 @@ async def mock_request( "desc": "Heating mode Comfort", } } - # HC2 status - inactive (value=0, desc=---) + # HC2 status - empty response (param not supported) if param_id == "8001": - return {"8001": {"value": "0", "desc": "---"}} - # HC3 status - empty response (param not supported) - if param_id == "8002": return {} return {} @@ -795,7 +697,7 @@ async def mock_request( "desc": "Heating mode Comfort", } } - # HC2 and HC3 fail with connection error + # HC2 fail with connection error msg = "Connection failed" raise BSBLANError(msg) @@ -857,8 +759,8 @@ async def mock_request( if param_id == "8000": msg = "Connection failed" raise BSBLANError(msg) - # HC2/HC3 return empty - if param_id in {"1000", "1300"}: + # HC2 return empty + if param_id == "1000": return {param_id: {}} return {} diff --git a/tests/test_constants.py b/tests/test_constants.py index 12a16642..076b68e0 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -153,9 +153,7 @@ def test_api_config_structure(version: str) -> None: "sensor", "hot_water", "heating_circuit2", - "heating_circuit3", "staticValues_circuit2", - "staticValues_circuit3", } assert set(config.keys()) == required_sections diff --git a/tests/test_hotwater_state.py b/tests/test_hotwater_state.py index 07d5f87f..ba5c0dc6 100644 --- a/tests/test_hotwater_state.py +++ b/tests/test_hotwater_state.py @@ -43,9 +43,7 @@ async def test_hot_water_state( if k not in ["561", "562", "563", "564", "565", "566", "567", "576"] }, "heating_circuit2": API_V3["heating_circuit2"].copy(), - "heating_circuit3": API_V3["heating_circuit3"].copy(), "staticValues_circuit2": API_V3["staticValues_circuit2"].copy(), - "staticValues_circuit3": API_V3["staticValues_circuit3"].copy(), } monkeypatch.setattr(bsblan, "_api_data", test_api_v3) diff --git a/tests/test_initialization.py b/tests/test_initialization.py index a8fd637e..2d4803b7 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -213,9 +213,7 @@ async def test_initialize_api_validator() -> None: "device": {}, "hot_water": {}, "heating_circuit2": {}, - "heating_circuit3": {}, "staticValues_circuit2": {}, - "staticValues_circuit3": {}, } # Create a coroutine mock for _validate_api_section that returns response data From eb2a5f49bedc8d82413084f623ce2a7725b4a769 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:29:03 +0100 Subject: [PATCH 03/10] refactor: remove support for triple heating circuit and update related tests --- examples/speed_test.py | 85 +----------------------------------------- 1 file changed, 1 insertion(+), 84 deletions(-) diff --git a/examples/speed_test.py b/examples/speed_test.py index 963ede7d..6a946343 100644 --- a/examples/speed_test.py +++ b/examples/speed_test.py @@ -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: @@ -59,26 +58,17 @@ HC1_PARAMS = ["700", "710", "900", "8000", "8740", "8749"] # 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", "8779"] # 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"] @@ -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( @@ -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, } From 4fb73d8b961fe9d2ba52f155d97b83a34299ac6c Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:34:37 +0100 Subject: [PATCH 04/10] chore: add uv.lock to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index cc0b120a..b00433e8 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,9 @@ ENV/ # ruff .ruff_cache +# uv +uv.lock + # Visual Studio Code .vscode From 4a96455163ff7e929a55c9d7bd30e1fff9532a5f Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:37:05 +0100 Subject: [PATCH 05/10] refactor: update invalid circuit error message to reflect supported circuits --- src/bsblan/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bsblan/constants.py b/src/bsblan/constants.py index 10463edd..5633d1b2 100644 --- a/src/bsblan/constants.py +++ b/src/bsblan/constants.py @@ -477,7 +477,7 @@ def get_hvac_action_category(status_code: int) -> HVACActionCategory: 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 number: {}. Must be 1 or 2." ) INVALID_RESPONSE_ERROR_MSG: Final[str] = ( "Invalid response format from BSB-LAN device: {}" From 66cc53c6c77c495fc9e12dbc1cb6ed3c96d7cf91 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:40:58 +0100 Subject: [PATCH 06/10] refactor: update documentation to reflect supported heating circuits (1 or 2) --- src/bsblan/bsblan.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bsblan/bsblan.py b/src/bsblan/bsblan.py index acb86482..d05fb02f 100644 --- a/src/bsblan/bsblan.py +++ b/src/bsblan/bsblan.py @@ -612,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). @@ -659,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. @@ -1009,8 +1009,8 @@ async def state( 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 + 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: @@ -1063,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. @@ -1155,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 @@ -1192,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. @@ -1234,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. From a56325aaf9074f2915e8160f080db9a11ee8a175 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 11:53:49 +0100 Subject: [PATCH 07/10] refactor: simplify invalid circuit error message format --- src/bsblan/constants.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bsblan/constants.py b/src/bsblan/constants.py index 5633d1b2..96ecff95 100644 --- a/src/bsblan/constants.py +++ b/src/bsblan/constants.py @@ -476,9 +476,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 or 2." -) +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: {}" ) From 1df2fbd9b9f1e7e4244b824df235585f906a917d Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 12:02:07 +0100 Subject: [PATCH 08/10] refactor: remove room1_thermostat_mode from heating parameters and state model --- src/bsblan/bsblan.py | 2 +- src/bsblan/constants.py | 2 -- src/bsblan/models.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/bsblan/bsblan.py b/src/bsblan/bsblan.py index d05fb02f..7a2c6544 100644 --- a/src/bsblan/bsblan.py +++ b/src/bsblan/bsblan.py @@ -1008,7 +1008,7 @@ 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. + 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. diff --git a/src/bsblan/constants.py b/src/bsblan/constants.py index 96ecff95..6a46cc5e 100644 --- a/src/bsblan/constants.py +++ b/src/bsblan/constants.py @@ -33,7 +33,6 @@ class APIConfig(TypedDict): # ------- "8000": "hvac_action", "8740": "current_temperature", - "8749": "room1_thermostat_mode", } BASE_STATIC_VALUES_PARAMS: Final[dict[str, str]] = { @@ -110,7 +109,6 @@ class APIConfig(TypedDict): # ------- "8001": "hvac_action", "8770": "current_temperature", - "8779": "room1_thermostat_mode", } BASE_STATIC_VALUES_CIRCUIT2_PARAMS: Final[dict[str, str]] = { diff --git a/src/bsblan/models.py b/src/bsblan/models.py index 9e93120e..df1f9a70 100644 --- a/src/bsblan/models.py +++ b/src/bsblan/models.py @@ -453,7 +453,6 @@ class State(BaseModel): hvac_action: EntityInfo[int] | None = None hvac_mode_changeover: EntityInfo[int] | None = None current_temperature: EntityInfo[float] | None = None - room1_thermostat_mode: EntityInfo[int] | None = None room1_temp_setpoint_boost: EntityInfo[float] | None = None From 04f08e0177d840f3f4cf2d847562c29f5e960e90 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 12:02:16 +0100 Subject: [PATCH 09/10] refactor: remove room thermostat mode parameters and update tests accordingly --- tests/fixtures/dict_version.json | 3 +-- tests/fixtures/state.json | 12 ------------ tests/fixtures/state_circuit2.json | 12 ------------ tests/test_state.py | 7 +------ 4 files changed, 2 insertions(+), 32 deletions(-) diff --git a/tests/fixtures/dict_version.json b/tests/fixtures/dict_version.json index 604f8c21..6ca744a3 100644 --- a/tests/fixtures/dict_version.json +++ b/tests/fixtures/dict_version.json @@ -4,8 +4,7 @@ "710": "target_temperature", "900": "hvac_mode_changeover", "8000": "hvac_action", - "8740": "current_temperature", - "8749": "room1_thermostat_mode" + "8740": "current_temperature" }, "staticValues": { "714": "min_temp", diff --git a/tests/fixtures/state.json b/tests/fixtures/state.json index d14cba11..3bad093f 100644 --- a/tests/fixtures/state.json +++ b/tests/fixtures/state.json @@ -61,18 +61,6 @@ "readwrite": 1, "unit": "°C" }, - "8749": { - "name": "Room thermostat heating circuit 1", - "dataType_name": "ENUM", - "dataType_family": "ENUM", - "error": 0, - "value": "0", - "desc": "No demand", - "dataType": 1, - "readonly": 1, - "readwrite": 1, - "unit": "" - }, "770": { "name": "Room temp setpoint boost (boost heating)", "dataType_name": "TEMP", diff --git a/tests/fixtures/state_circuit2.json b/tests/fixtures/state_circuit2.json index cad2444c..1212c3b2 100644 --- a/tests/fixtures/state_circuit2.json +++ b/tests/fixtures/state_circuit2.json @@ -61,18 +61,6 @@ "readwrite": 1, "unit": "°C" }, - "8779": { - "name": "Room thermostat heating circuit 2", - "dataType_name": "ENUM", - "dataType_family": "ENUM", - "error": 0, - "value": "0", - "desc": "No demand", - "dataType": 1, - "readonly": 1, - "readwrite": 1, - "unit": "" - }, "1070": { "name": "Room temp setpoint boost (boost heating)", "dataType_name": "TEMP", diff --git a/tests/test_state.py b/tests/test_state.py index 1095d653..83cc8d4f 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -74,11 +74,6 @@ async def test_state(monkeypatch: Any) -> None: assert state.current_temperature.value == 19.3 assert state.current_temperature.unit == "°C" - # Room thermostat mode assertions - assert state.room1_thermostat_mode is not None - assert state.room1_thermostat_mode.value == 0 - assert state.room1_thermostat_mode.desc == "No demand" - # Room temperature setpoint boost assertions assert state.room1_temp_setpoint_boost is not None assert state.room1_temp_setpoint_boost.value is None @@ -86,5 +81,5 @@ async def test_state(monkeypatch: Any) -> None: # Verify API call request_mock.assert_called_once_with( - params={"Parameter": "700,710,900,8000,8740,8749,770"} + params={"Parameter": "700,710,900,8000,8740,770"} ) From bba863248d1bbbc440b034033e6a82e76477ff08 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 25 Mar 2026 12:02:21 +0100 Subject: [PATCH 10/10] refactor: remove redundant parameters from heating circuit definitions --- examples/speed_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/speed_test.py b/examples/speed_test.py index 6a946343..c5e190ea 100644 --- a/examples/speed_test.py +++ b/examples/speed_test.py @@ -55,10 +55,10 @@ 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", "8770", "8779"] +HC2_PARAMS = ["1000", "1010", "1200", "8001", "8770"] # Static values per circuit HC1_STATIC_PARAMS = ["714", "716"]