diff --git a/homeassistant/components/liebherr/__init__.py b/homeassistant/components/liebherr/__init__.py index 21de6d09a08..90c0c953ffa 100644 --- a/homeassistant/components/liebherr/__init__.py +++ b/homeassistant/components/liebherr/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio +from datetime import datetime +import logging from pyliebherrhomeapi import LiebherrClient from pyliebherrhomeapi.exceptions import ( @@ -14,8 +16,13 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval -from .coordinator import LiebherrConfigEntry, LiebherrCoordinator +from .const import DEVICE_SCAN_INTERVAL, DOMAIN +from .coordinator import LiebherrConfigEntry, LiebherrCoordinator, LiebherrData + +_LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ Platform.NUMBER, @@ -42,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> raise ConfigEntryNotReady(f"Failed to connect to Liebherr API: {err}") from err # Create a coordinator for each device (may be empty if no devices) - coordinators: dict[str, LiebherrCoordinator] = {} + data = LiebherrData(client=client) for device in devices: coordinator = LiebherrCoordinator( hass=hass, @@ -50,20 +57,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> client=client, device_id=device.device_id, ) - coordinators[device.device_id] = coordinator + data.coordinators[device.device_id] = coordinator await asyncio.gather( *( coordinator.async_config_entry_first_refresh() - for coordinator in coordinators.values() + for coordinator in data.coordinators.values() ) ) - # Store coordinators in runtime data - entry.runtime_data = coordinators + # Store runtime data + entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Schedule periodic scan for new devices + async def _async_scan_for_new_devices(_now: datetime) -> None: + """Scan for new devices added to the account.""" + try: + devices = await client.get_devices() + except LiebherrAuthenticationError, LiebherrConnectionError: + _LOGGER.debug("Failed to scan for new devices") + return + except Exception: + _LOGGER.exception("Unexpected error scanning for new devices") + return + + new_coordinators: list[LiebherrCoordinator] = [] + for device in devices: + if device.device_id not in data.coordinators: + coordinator = LiebherrCoordinator( + hass=hass, + config_entry=entry, + client=client, + device_id=device.device_id, + ) + await coordinator.async_refresh() + if not coordinator.last_update_success: + _LOGGER.debug("Failed to set up new device %s", device.device_id) + continue + data.coordinators[device.device_id] = coordinator + new_coordinators.append(coordinator) + + if new_coordinators: + async_dispatcher_send( + hass, + f"{DOMAIN}_new_device_{entry.entry_id}", + new_coordinators, + ) + + entry.async_on_unload( + async_track_time_interval( + hass, _async_scan_for_new_devices, DEVICE_SCAN_INTERVAL + ) + ) + return True diff --git a/homeassistant/components/liebherr/const.py b/homeassistant/components/liebherr/const.py index 82af6817c09..ceffd331d66 100644 --- a/homeassistant/components/liebherr/const.py +++ b/homeassistant/components/liebherr/const.py @@ -6,4 +6,6 @@ from typing import Final DOMAIN: Final = "liebherr" MANUFACTURER: Final = "Liebherr" +SCAN_INTERVAL: Final = timedelta(seconds=60) +DEVICE_SCAN_INTERVAL: Final = timedelta(minutes=5) REFRESH_DELAY: Final = timedelta(seconds=5) diff --git a/homeassistant/components/liebherr/coordinator.py b/homeassistant/components/liebherr/coordinator.py index c840237371d..1364149f2c5 100644 --- a/homeassistant/components/liebherr/coordinator.py +++ b/homeassistant/components/liebherr/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import timedelta +from dataclasses import dataclass, field import logging from pyliebherrhomeapi import ( @@ -18,13 +18,20 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN - -type LiebherrConfigEntry = ConfigEntry[dict[str, LiebherrCoordinator]] +from .const import DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) + +@dataclass +class LiebherrData: + """Runtime data for the Liebherr integration.""" + + client: LiebherrClient + coordinators: dict[str, LiebherrCoordinator] = field(default_factory=dict) + + +type LiebherrConfigEntry = ConfigEntry[LiebherrData] class LiebherrCoordinator(DataUpdateCoordinator[DeviceState]): diff --git a/homeassistant/components/liebherr/diagnostics.py b/homeassistant/components/liebherr/diagnostics.py index 21e6ab7af4c..a86b52aac91 100644 --- a/homeassistant/components/liebherr/diagnostics.py +++ b/homeassistant/components/liebherr/diagnostics.py @@ -29,6 +29,6 @@ async def async_get_config_entry_diagnostics( }, "data": asdict(coordinator.data), } - for device_id, coordinator in entry.runtime_data.items() + for device_id, coordinator in entry.runtime_data.coordinators.items() }, } diff --git a/homeassistant/components/liebherr/number.py b/homeassistant/components/liebherr/number.py index 6ba938e0a2c..46a44e23d08 100644 --- a/homeassistant/components/liebherr/number.py +++ b/homeassistant/components/liebherr/number.py @@ -16,9 +16,11 @@ from homeassistant.components.number import ( NumberEntityDescription, ) from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import DOMAIN from .coordinator import LiebherrConfigEntry, LiebherrCoordinator from .entity import LiebherrZoneEntity @@ -53,22 +55,41 @@ NUMBER_TYPES: tuple[LiebherrNumberEntityDescription, ...] = ( ) +def _create_number_entities( + coordinators: list[LiebherrCoordinator], +) -> list[LiebherrNumber]: + """Create number entities for the given coordinators.""" + return [ + LiebherrNumber( + coordinator=coordinator, + zone_id=temp_control.zone_id, + description=description, + ) + for coordinator in coordinators + for temp_control in coordinator.data.get_temperature_controls().values() + for description in NUMBER_TYPES + ] + + async def async_setup_entry( hass: HomeAssistant, entry: LiebherrConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Liebherr number entities.""" - coordinators = entry.runtime_data async_add_entities( - LiebherrNumber( - coordinator=coordinator, - zone_id=temp_control.zone_id, - description=description, + _create_number_entities(list(entry.runtime_data.coordinators.values())) + ) + + @callback + def _async_new_device(coordinators: list[LiebherrCoordinator]) -> None: + """Add number entities for new devices.""" + async_add_entities(_create_number_entities(coordinators)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_device_{entry.entry_id}", _async_new_device ) - for coordinator in coordinators.values() - for temp_control in coordinator.data.get_temperature_controls().values() - for description in NUMBER_TYPES ) diff --git a/homeassistant/components/liebherr/quality_scale.yaml b/homeassistant/components/liebherr/quality_scale.yaml index 1d24e92c1df..4656c2d9e7d 100644 --- a/homeassistant/components/liebherr/quality_scale.yaml +++ b/homeassistant/components/liebherr/quality_scale.yaml @@ -53,7 +53,7 @@ rules: docs-supported-functions: done docs-troubleshooting: done docs-use-cases: done - dynamic-devices: todo + dynamic-devices: done entity-category: done entity-device-class: done entity-disabled-by-default: diff --git a/homeassistant/components/liebherr/select.py b/homeassistant/components/liebherr/select.py index f8eec6c3b30..66166a30fed 100644 --- a/homeassistant/components/liebherr/select.py +++ b/homeassistant/components/liebherr/select.py @@ -18,9 +18,11 @@ from pyliebherrhomeapi import ( ) from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import DOMAIN from .coordinator import LiebherrConfigEntry, LiebherrCoordinator from .entity import ZONE_POSITION_MAP, LiebherrEntity @@ -109,15 +111,13 @@ SELECT_TYPES: list[LiebherrSelectEntityDescription] = [ ] -async def async_setup_entry( - hass: HomeAssistant, - entry: LiebherrConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Liebherr select entities.""" +def _create_select_entities( + coordinators: list[LiebherrCoordinator], +) -> list[LiebherrSelectEntity]: + """Create select entities for the given coordinators.""" entities: list[LiebherrSelectEntity] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in coordinators: has_multiple_zones = len(coordinator.data.get_temperature_controls()) > 1 for control in coordinator.data.controls: @@ -137,7 +137,29 @@ async def async_setup_entry( ) ) - async_add_entities(entities) + return entities + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LiebherrConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Liebherr select entities.""" + async_add_entities( + _create_select_entities(list(entry.runtime_data.coordinators.values())) + ) + + @callback + def _async_new_device(coordinators: list[LiebherrCoordinator]) -> None: + """Add select entities for new devices.""" + async_add_entities(_create_select_entities(coordinators)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_device_{entry.entry_id}", _async_new_device + ) + ) class LiebherrSelectEntity(LiebherrEntity, SelectEntity): diff --git a/homeassistant/components/liebherr/sensor.py b/homeassistant/components/liebherr/sensor.py index aeffe616414..1f4fb09dc49 100644 --- a/homeassistant/components/liebherr/sensor.py +++ b/homeassistant/components/liebherr/sensor.py @@ -14,10 +14,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from .const import DOMAIN from .coordinator import LiebherrConfigEntry, LiebherrCoordinator from .entity import LiebherrZoneEntity @@ -48,22 +50,41 @@ SENSOR_TYPES: tuple[LiebherrSensorEntityDescription, ...] = ( ) +def _create_sensor_entities( + coordinators: list[LiebherrCoordinator], +) -> list[LiebherrSensor]: + """Create sensor entities for the given coordinators.""" + return [ + LiebherrSensor( + coordinator=coordinator, + zone_id=temp_control.zone_id, + description=description, + ) + for coordinator in coordinators + for temp_control in coordinator.data.get_temperature_controls().values() + for description in SENSOR_TYPES + ] + + async def async_setup_entry( hass: HomeAssistant, entry: LiebherrConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Liebherr sensor entities.""" - coordinators = entry.runtime_data async_add_entities( - LiebherrSensor( - coordinator=coordinator, - zone_id=temp_control.zone_id, - description=description, + _create_sensor_entities(list(entry.runtime_data.coordinators.values())) + ) + + @callback + def _async_new_device(coordinators: list[LiebherrCoordinator]) -> None: + """Add sensor entities for new devices.""" + async_add_entities(_create_sensor_entities(coordinators)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_device_{entry.entry_id}", _async_new_device ) - for coordinator in coordinators.values() - for temp_control in coordinator.data.get_temperature_controls().values() - for description in SENSOR_TYPES ) diff --git a/homeassistant/components/liebherr/switch.py b/homeassistant/components/liebherr/switch.py index 8780025cf5f..aba8da3f418 100644 --- a/homeassistant/components/liebherr/switch.py +++ b/homeassistant/components/liebherr/switch.py @@ -15,9 +15,11 @@ from pyliebherrhomeapi.const import ( ) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import DOMAIN from .coordinator import LiebherrConfigEntry, LiebherrCoordinator from .entity import ZONE_POSITION_MAP, LiebherrEntity @@ -90,15 +92,13 @@ DEVICE_SWITCH_TYPES: dict[str, LiebherrDeviceSwitchEntityDescription] = { } -async def async_setup_entry( - hass: HomeAssistant, - entry: LiebherrConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Liebherr switch entities.""" +def _create_switch_entities( + coordinators: list[LiebherrCoordinator], +) -> list[LiebherrDeviceSwitch | LiebherrZoneSwitch]: + """Create switch entities for the given coordinators.""" entities: list[LiebherrDeviceSwitch | LiebherrZoneSwitch] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in coordinators: has_multiple_zones = len(coordinator.data.get_temperature_controls()) > 1 for control in coordinator.data.controls: @@ -127,7 +127,29 @@ async def async_setup_entry( ) ) - async_add_entities(entities) + return entities + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LiebherrConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Liebherr switch entities.""" + async_add_entities( + _create_switch_entities(list(entry.runtime_data.coordinators.values())) + ) + + @callback + def _async_new_device(coordinators: list[LiebherrCoordinator]) -> None: + """Add switch entities for new devices.""" + async_add_entities(_create_switch_entities(coordinators)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_device_{entry.entry_id}", _async_new_device + ) + ) class LiebherrDeviceSwitch(LiebherrEntity, SwitchEntity): diff --git a/tests/components/liebherr/test_init.py b/tests/components/liebherr/test_init.py index 8677849c083..21e4a84d785 100644 --- a/tests/components/liebherr/test_init.py +++ b/tests/components/liebherr/test_init.py @@ -1,20 +1,36 @@ """Test the liebherr integration init.""" +import copy +from datetime import timedelta from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory +from pyliebherrhomeapi import ( + Device, + DeviceState, + DeviceType, + IceMakerControl, + IceMakerMode, + TemperatureControl, + TemperatureUnit, + ToggleControl, + ZonePosition, +) from pyliebherrhomeapi.exceptions import ( LiebherrAuthenticationError, LiebherrConnectionError, ) import pytest +from homeassistant.components.liebherr.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .conftest import MOCK_DEVICE +from .conftest import MOCK_DEVICE, MOCK_DEVICE_STATE -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed # Test errors during initial get_devices() call in async_setup_entry @@ -85,3 +101,173 @@ async def test_unload_entry( await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +NEW_DEVICE = Device( + device_id="new_device_id", + nickname="New Fridge", + device_type=DeviceType.FRIDGE, + device_name="K2601", +) + +NEW_DEVICE_STATE = DeviceState( + device=NEW_DEVICE, + controls=[ + TemperatureControl( + zone_id=1, + zone_position=ZonePosition.TOP, + name="Fridge", + type="fridge", + value=4, + target=5, + min=2, + max=8, + unit=TemperatureUnit.CELSIUS, + ), + ToggleControl( + name="supercool", + type="ToggleControl", + zone_id=1, + zone_position=ZonePosition.TOP, + value=False, + ), + IceMakerControl( + name="icemaker", + type="IceMakerControl", + zone_id=1, + zone_position=ZonePosition.TOP, + ice_maker_mode=IceMakerMode.OFF, + has_max_ice=False, + ), + ], +) + + +@pytest.mark.usefixtures("init_integration") +async def test_dynamic_device_discovery_no_new_devices( + hass: HomeAssistant, + mock_liebherr_client: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test device scan with no new devices does not create entities.""" + # Same devices returned + mock_liebherr_client.get_devices.return_value = [MOCK_DEVICE] + + initial_states = len(hass.states.async_all()) + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # No new entities should be created + assert len(hass.states.async_all()) == initial_states + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + "exception", + [ + LiebherrConnectionError("Connection failed"), + LiebherrAuthenticationError("Auth failed"), + ], + ids=["connection_error", "auth_error"], +) +async def test_dynamic_device_discovery_api_error( + hass: HomeAssistant, + mock_liebherr_client: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + exception: Exception, +) -> None: + """Test device scan gracefully handles API errors.""" + mock_liebherr_client.get_devices.side_effect = exception + + initial_states = len(hass.states.async_all()) + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # No crash, no new entities + assert len(hass.states.async_all()) == initial_states + assert mock_config_entry.state is ConfigEntryState.LOADED + + +@pytest.mark.usefixtures("init_integration") +async def test_dynamic_device_discovery_coordinator_setup_failure( + hass: HomeAssistant, + mock_liebherr_client: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test device scan skips devices that fail coordinator setup.""" + # New device appears but its state fetch fails + mock_liebherr_client.get_devices.return_value = [MOCK_DEVICE, NEW_DEVICE] + + original_state = copy.deepcopy(MOCK_DEVICE_STATE) + mock_liebherr_client.get_device_state.side_effect = lambda device_id, **kw: ( + copy.deepcopy(original_state) + if device_id == "test_device_id" + else (_ for _ in ()).throw(LiebherrConnectionError("Device offline")) + ) + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # New device should NOT be added + assert "new_device_id" not in mock_config_entry.runtime_data.coordinators + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_dynamic_device_discovery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_liebherr_client: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test new devices are automatically discovered on all platforms.""" + mock_config_entry.add_to_hass(hass) + + all_platforms = [ + Platform.SENSOR, + Platform.NUMBER, + Platform.SWITCH, + Platform.SELECT, + ] + with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", all_platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Initially only the original device exists + assert hass.states.get("sensor.test_fridge_top_zone") is not None + assert hass.states.get("sensor.new_fridge") is None + + # Simulate a new device appearing on the account + mock_liebherr_client.get_devices.return_value = [MOCK_DEVICE, NEW_DEVICE] + mock_liebherr_client.get_device_state.side_effect = lambda device_id, **kw: ( + copy.deepcopy( + NEW_DEVICE_STATE if device_id == "new_device_id" else MOCK_DEVICE_STATE + ) + ) + + # Advance time to trigger device scan (5 minute interval) + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # New device should have entities on all platforms + state = hass.states.get("sensor.new_fridge") + assert state is not None + assert state.state == "4" + assert hass.states.get("number.new_fridge_setpoint") is not None + assert hass.states.get("switch.new_fridge_supercool") is not None + assert hass.states.get("select.new_fridge_icemaker") is not None + + # Original device should still exist + assert hass.states.get("sensor.test_fridge_top_zone") is not None + + # Runtime data should have both coordinators + assert "new_device_id" in mock_config_entry.runtime_data.coordinators + assert "test_device_id" in mock_config_entry.runtime_data.coordinators diff --git a/tests/components/liebherr/test_number.py b/tests/components/liebherr/test_number.py index 95ccdc6bfa8..a20116621e7 100644 --- a/tests/components/liebherr/test_number.py +++ b/tests/components/liebherr/test_number.py @@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .conftest import MOCK_DEVICE, MOCK_DEVICE_STATE +from .conftest import MOCK_DEVICE from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -100,70 +100,6 @@ async def test_single_zone_number( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_multi_zone_with_none_position( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_liebherr_client: MagicMock, - mock_config_entry: MockConfigEntry, - platforms: list[Platform], -) -> None: - """Test multi-zone device with None zone_position falls back to base translation key.""" - device = Device( - device_id="multi_zone_none", - nickname="Multi Zone Fridge", - device_type=DeviceType.COMBI, - device_name="CBNes9999", - ) - mock_liebherr_client.get_devices.return_value = [device] - multi_zone_state = DeviceState( - device=device, - controls=[ - TemperatureControl( - zone_id=1, - zone_position=None, # None triggers fallback - name="Fridge", - type="fridge", - value=5, - target=4, - min=2, - max=8, - unit=TemperatureUnit.CELSIUS, - ), - TemperatureControl( - zone_id=2, - zone_position=ZonePosition.BOTTOM, - name="Freezer", - type="freezer", - value=-18, - target=-18, - min=-24, - max=-16, - unit=TemperatureUnit.CELSIUS, - ), - ], - ) - mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy( - multi_zone_state - ) - - mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.liebherr.PLATFORMS", platforms): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - # Zone with None position should have base translation key - zone1_entity = entity_registry.async_get("number.multi_zone_fridge_setpoint") - assert zone1_entity is not None - assert zone1_entity.translation_key == "setpoint_temperature" - - # Zone with valid position should have zone-specific translation key - zone2_entity = entity_registry.async_get( - "number.multi_zone_fridge_bottom_zone_setpoint" - ) - assert zone2_entity is not None - assert zone2_entity.translation_key == "setpoint_temperature_bottom_zone" - - @pytest.mark.usefixtures("init_integration") async def test_set_temperature( hass: HomeAssistant, @@ -216,50 +152,6 @@ async def test_set_temperature_failure( ) -@pytest.mark.usefixtures("init_integration") -async def test_number_update_failure( - hass: HomeAssistant, - mock_liebherr_client: MagicMock, - freezer: FrozenDateTimeFactory, -) -> None: - """Test number becomes unavailable when coordinator update fails and recovers.""" - entity_id = "number.test_fridge_top_zone_setpoint" - - # Initial state should be available with value - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "4" - - # Simulate update error - mock_liebherr_client.get_device_state.side_effect = LiebherrConnectionError( - "Connection failed" - ) - - # Advance time to trigger coordinator refresh (60 second interval) - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - # Number should now be unavailable - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Simulate recovery - mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy( - MOCK_DEVICE_STATE - ) - - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - # Number should recover - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "4" - - @pytest.mark.usefixtures("init_integration") async def test_number_when_control_missing( hass: HomeAssistant, diff --git a/tests/components/liebherr/test_select.py b/tests/components/liebherr/test_select.py index 7a22fe4ff50..76023a5bc21 100644 --- a/tests/components/liebherr/test_select.py +++ b/tests/components/liebherr/test_select.py @@ -182,46 +182,6 @@ async def test_select_failure( ) -@pytest.mark.usefixtures("init_integration") -async def test_select_update_failure( - hass: HomeAssistant, - mock_liebherr_client: MagicMock, - freezer: FrozenDateTimeFactory, -) -> None: - """Test select becomes unavailable when coordinator update fails and recovers.""" - entity_id = "select.test_fridge_bottom_zone_icemaker" - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "off" - - # Simulate update error - mock_liebherr_client.get_device_state.side_effect = LiebherrConnectionError( - "Connection failed" - ) - - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Simulate recovery - mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy( - MOCK_DEVICE_STATE - ) - - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "off" - - @pytest.mark.usefixtures("init_integration") async def test_select_when_control_missing( hass: HomeAssistant, @@ -307,70 +267,6 @@ async def test_single_zone_select( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_multi_zone_with_none_position( - hass: HomeAssistant, - mock_liebherr_client: MagicMock, - mock_config_entry: MockConfigEntry, - platforms: list[Platform], -) -> None: - """Test multi-zone device where zone_position is None.""" - device = Device( - device_id="multi_none_id", - nickname="Multi None Fridge", - device_type=DeviceType.COMBI, - device_name="CBNes5678", - ) - mock_liebherr_client.get_devices.return_value = [device] - state = DeviceState( - device=device, - controls=[ - TemperatureControl( - zone_id=1, - zone_position=None, - name="Fridge", - type="fridge", - value=4, - target=4, - min=2, - max=8, - unit=TemperatureUnit.CELSIUS, - ), - TemperatureControl( - zone_id=2, - zone_position=None, - name="Freezer", - type="freezer", - value=-18, - target=-18, - min=-24, - max=-16, - unit=TemperatureUnit.CELSIUS, - ), - IceMakerControl( - name="icemaker", - type="IceMakerControl", - zone_id=1, - zone_position=None, - ice_maker_mode=IceMakerMode.OFF, - has_max_ice=True, - ), - ], - ) - mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy( - state - ) - - mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.liebherr.PLATFORMS", platforms): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - # Without zone_position, should use the base translation key (no zone suffix) - entity_state = hass.states.get("select.multi_none_fridge_icemaker") - assert entity_state is not None - assert entity_state.state == "off" - - @pytest.mark.usefixtures("init_integration") async def test_select_current_option_none_mode( hass: HomeAssistant, diff --git a/tests/components/liebherr/test_switch.py b/tests/components/liebherr/test_switch.py index 2f6866fffe9..51ed0d6948d 100644 --- a/tests/components/liebherr/test_switch.py +++ b/tests/components/liebherr/test_switch.py @@ -32,7 +32,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .conftest import MOCK_DEVICE, MOCK_DEVICE_STATE +from .conftest import MOCK_DEVICE from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -150,46 +150,6 @@ async def test_switch_failure( ) -@pytest.mark.usefixtures("init_integration") -async def test_switch_update_failure( - hass: HomeAssistant, - mock_liebherr_client: MagicMock, - freezer: FrozenDateTimeFactory, -) -> None: - """Test switch becomes unavailable when coordinator update fails and recovers.""" - entity_id = "switch.test_fridge_top_zone_supercool" - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_OFF - - # Simulate update error - mock_liebherr_client.get_device_state.side_effect = LiebherrConnectionError( - "Connection failed" - ) - - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Simulate recovery - mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy( - MOCK_DEVICE_STATE - ) - - freezer.tick(timedelta(seconds=61)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_OFF - - @pytest.mark.usefixtures("init_integration") async def test_switch_when_control_missing( hass: HomeAssistant,