diff --git a/src/bsblan/bsblan.py b/src/bsblan/bsblan.py index 877ad9a1..1a005052 100644 --- a/src/bsblan/bsblan.py +++ b/src/bsblan/bsblan.py @@ -152,13 +152,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): - 1. Query the operating mode parameter — the response must be - non-empty and contain actual data. - 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. + Uses the configured operating mode probe parameters from + CircuitConfig.PROBE_PARAMS as the only discovery signal. Status + parameters are not queried during discovery to keep setup lightweight + and avoid excluding valid circuits when status data is unavailable. This is useful for integration setup flows (e.g., Home Assistant config flow) to discover how many circuits the user's controller @@ -179,46 +176,23 @@ async def get_available_circuits(self) -> list[int]: response = await self._request( params={"Parameter": param_id}, ) - # A circuit exists if the response contains the param_id key - # with actual data (not an empty dict) - if not response.get(param_id): - continue - - # Secondary check: query the status parameter. - # Inactive circuits either: - # - return value="0" and desc="---" - # - return an empty dict {} (param not supported) - status_id = CircuitConfig.STATUS_PARAMS[circuit] - status_resp = await self._request( - params={"Parameter": status_id}, + except BSBLANError: + logger.debug( + "Circuit %d not available (operating mode request failed)", + circuit, ) - status_data = status_resp.get(status_id, {}) - - # Empty response means the parameter doesn't exist - if not status_data or not isinstance(status_data, dict): - logger.debug( - "Circuit %d has no status data (not supported)", - circuit, - ) - continue - - # value="0" + desc="---" means inactive - if ( - status_data.get("desc") == CircuitConfig.INACTIVE_MARKER - and str(status_data.get("value", "")) == "0" - ): - logger.debug( - "Circuit %d has status '---' (inactive)", - circuit, - ) - continue + continue - available.append(circuit) - except BSBLANError: + # A circuit exists if the response contains the operating mode key + # with actual data (not an empty dict). + if not response.get(param_id): logger.debug( - "Circuit %d not available (request failed)", + "Circuit %d has no operating mode data (not supported)", circuit, ) + continue + + available.append(circuit) return sorted(available) async def _setup_api_validator(self) -> None: diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 66622287..c0c1dd9b 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -13,7 +13,7 @@ from aresponses import Response, ResponsesMockServer from bsblan import BSBLAN, BSBLANConfig, State, StaticState -from bsblan.constants import ErrorMsg, build_api_config +from bsblan.constants import CircuitConfig, ErrorMsg, build_api_config from bsblan.exceptions import BSBLANError, BSBLANInvalidParameterError from bsblan.utility import APIValidator @@ -544,26 +544,20 @@ async def mock_request( # HC1 operating mode if param_id == "700": return {"700": {"value": "1", "unit": "", "desc": "Automatic"}} - # HC1 status - active - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } # HC2 operating mode if param_id == "1000": return {"1000": {"value": "1", "unit": "", "desc": "Automatic"}} - # HC2 status - active - if param_id == "8001": - return {"8001": {"value": "114", "desc": "Heating mode Comfort"}} - return {} + msg = f"Unexpected parameter probe: {param_id}" + raise AssertionError(msg) - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] + request_mock = AsyncMock(side_effect=mock_request) + bsblan._request = request_mock # type: ignore[method-assign] circuits = await bsblan.get_available_circuits() assert circuits == [1, 2] + assert [ + call.kwargs["params"]["Parameter"] for call in request_mock.await_args_list + ] == ["700", "1000"] @pytest.mark.asyncio @@ -580,33 +574,27 @@ async def mock_request( param_id = params.get("Parameter", "") if param_id == "700": return {"700": {"value": "3", "unit": "", "desc": "Comfort"}} - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } # HC2 operating mode - return empty if param_id == "1000": return {param_id: {}} - return {} + msg = f"Unexpected parameter probe: {param_id}" + raise AssertionError(msg) - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] + request_mock = AsyncMock(side_effect=mock_request) + bsblan._request = request_mock # type: ignore[method-assign] circuits = await bsblan.get_available_circuits() assert circuits == [1] + assert [ + call.kwargs["params"]["Parameter"] for call in request_mock.await_args_list + ] == ["700", "1000"] @pytest.mark.asyncio -async def test_get_available_circuits_inactive_by_status( +async def test_get_available_circuits_does_not_probe_status_params( mock_bsblan_circuit: BSBLAN, ) -> None: - """Test that circuits with status '---' are detected as inactive. - - This is the real-world scenario: the device returns a valid operating - mode for all circuits, but status param shows '---' for HC2. - """ + """Test discovery only queries probe params, not status params.""" bsblan = mock_bsblan_circuit async def mock_request( @@ -617,61 +605,20 @@ async def mock_request( # All circuits return valid operating mode if param_id in {"700", "1000"}: return {param_id: {"value": "1", "unit": "", "desc": "Automatic"}} - # HC1 status - active - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } - # HC2 status - inactive (value=0, desc=---) - if param_id == "8001": - return {"8001": {"value": "0", "desc": "---"}} - return {} + msg = f"Unexpected status parameter probe: {param_id}" + raise AssertionError(msg) - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] + request_mock = AsyncMock(side_effect=mock_request) + bsblan._request = request_mock # type: ignore[method-assign] circuits = await bsblan.get_available_circuits() - assert circuits == [1] - - -@pytest.mark.asyncio -async def test_get_available_circuits_inactive_empty_status( - mock_bsblan_circuit: BSBLAN, -) -> None: - """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., HC2 status 8001 returns {}). - """ - bsblan = mock_bsblan_circuit - - async def mock_request( - **kwargs: Any, - ) -> dict[str, Any]: - params = kwargs.get("params", {}) - param_id = params.get("Parameter", "") - # All circuits return valid operating mode - if param_id in {"700", "1000"}: - return {param_id: {"value": "1", "unit": "", "desc": "Automatic"}} - # HC1 status - active - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } - # HC2 status - empty response (param not supported) - if param_id == "8001": - return {} - return {} - - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] - - circuits = await bsblan.get_available_circuits() - assert circuits == [1] + assert circuits == [1, 2] + assert { + call.kwargs["params"]["Parameter"] for call in request_mock.await_args_list + } == { + "700", + "1000", + } @pytest.mark.asyncio @@ -681,27 +628,19 @@ async def test_get_available_circuits_request_failure( """Test circuit detection when some requests fail.""" bsblan = mock_bsblan_circuit - call_count = 0 - async def mock_request( **kwargs: Any, ) -> dict[str, Any]: - nonlocal call_count - call_count += 1 params = kwargs.get("params", {}) param_id = params.get("Parameter", "") if param_id == "700": return {"700": {"value": "1", "unit": "", "desc": "Automatic"}} - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } # HC2 fail with connection error - msg = "Connection failed" - raise BSBLANError(msg) + if param_id == "1000": + msg = "Connection failed" + raise BSBLANError(msg) + msg = f"Unexpected parameter probe: {param_id}" + raise AssertionError(msg) bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] @@ -723,15 +662,11 @@ async def mock_request( param_id = params.get("Parameter", "") if param_id == "700": return {"700": {"value": "1", "unit": "", "desc": "Automatic"}} - if param_id == "8000": - return { - "8000": { - "value": "114", - "desc": "Heating mode Comfort", - } - } # Returns a response but without the expected param key - return {"other_key": {"value": "1"}} + if param_id == "1000": + return {"other_key": {"value": "1"}} + msg = f"Unexpected parameter probe: {param_id}" + raise AssertionError(msg) bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] @@ -740,37 +675,31 @@ async def mock_request( @pytest.mark.asyncio -async def test_get_available_circuits_status_failure_excludes_circuit( +async def test_get_available_circuits_all_probes_missing( mock_bsblan_circuit: BSBLAN, ) -> None: - """Test that a circuit is excluded if the status request fails. - - If the operating mode returns valid data but the status request fails, - the circuit should be excluded (fail-safe). - """ + """Test no circuits are detected when operating mode probes are missing.""" bsblan = mock_bsblan_circuit + expected_params = list(CircuitConfig.PROBE_PARAMS.values()) async def mock_request( **kwargs: Any, ) -> dict[str, Any]: params = kwargs.get("params", {}) param_id = params.get("Parameter", "") - if param_id == "700": - return {"700": {"value": "1", "unit": "", "desc": "Automatic"}} - # Status request fails - if param_id == "8000": - msg = "Connection failed" - raise BSBLANError(msg) - # HC2 return empty - if param_id == "1000": - return {param_id: {}} - return {} + if param_id in expected_params: + return {} + msg = f"Unexpected parameter probe: {param_id}" + raise AssertionError(msg) - bsblan._request = AsyncMock(side_effect=mock_request) # type: ignore[method-assign] + request_mock = AsyncMock(side_effect=mock_request) + bsblan._request = request_mock # type: ignore[method-assign] - # HC1 status request fails -> entire circuit probe fails -> excluded circuits = await bsblan.get_available_circuits() assert circuits == [] + assert [ + call.kwargs["params"]["Parameter"] for call in request_mock.await_args_list + ] == expected_params @pytest.mark.asyncio