1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Implement dynamic devices for Liebherr integration (#163951)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
mettolen
2026-02-25 00:32:51 +02:00
committed by GitHub
parent 697441969b
commit e671e4408b
13 changed files with 383 additions and 306 deletions
+54 -6
View File
@@ -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
@@ -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)
@@ -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]):
@@ -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()
},
}
+30 -9
View File
@@ -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
)
@@ -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:
+31 -9
View File
@@ -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):
+30 -9
View File
@@ -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
)
+31 -9
View File
@@ -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):
+189 -3
View File
@@ -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
+1 -109
View File
@@ -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,
-104
View File
@@ -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,
+1 -41
View File
@@ -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,