Add select entities to Liebherr integration (#163581)

This commit is contained in:
mettolen
2026-02-23 21:52:50 +01:00
committed by GitHub
parent fb118ed516
commit 8f2bfa1bb0
8 changed files with 1150 additions and 2 deletions
@@ -1,4 +1,4 @@
"""The liebherr integration."""
"""The Liebherr integration."""
from __future__ import annotations
@@ -17,7 +17,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> bool:
@@ -1,5 +1,55 @@
{
"entity": {
"select": {
"bio_fresh_plus": {
"default": "mdi:leaf"
},
"bio_fresh_plus_bottom_zone": {
"default": "mdi:leaf"
},
"bio_fresh_plus_middle_zone": {
"default": "mdi:leaf"
},
"bio_fresh_plus_top_zone": {
"default": "mdi:leaf"
},
"hydro_breeze": {
"default": "mdi:weather-windy"
},
"hydro_breeze_bottom_zone": {
"default": "mdi:weather-windy"
},
"hydro_breeze_middle_zone": {
"default": "mdi:weather-windy"
},
"hydro_breeze_top_zone": {
"default": "mdi:weather-windy"
},
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
}
}
},
"switch": {
"night_mode": {
"default": "mdi:sleep",
+216
View File
@@ -0,0 +1,216 @@
"""Select platform for Liebherr integration."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from enum import StrEnum
from typing import TYPE_CHECKING, Any
from pyliebherrhomeapi import (
BioFreshPlusControl,
BioFreshPlusMode,
HydroBreezeControl,
HydroBreezeMode,
IceMakerControl,
IceMakerMode,
ZonePosition,
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
from .entity import ZONE_POSITION_MAP, LiebherrEntity
PARALLEL_UPDATES = 1
type SelectControl = IceMakerControl | HydroBreezeControl | BioFreshPlusControl
@dataclass(frozen=True, kw_only=True)
class LiebherrSelectEntityDescription(SelectEntityDescription):
"""Describes a Liebherr select entity."""
control_type: type[SelectControl]
mode_enum: type[StrEnum]
current_mode_fn: Callable[[SelectControl], StrEnum | str | None]
options_fn: Callable[[SelectControl], list[str]]
set_fn: Callable[[LiebherrCoordinator, int, StrEnum], Coroutine[Any, Any, None]]
def _ice_maker_options(control: SelectControl) -> list[str]:
"""Return available ice maker options."""
if TYPE_CHECKING:
assert isinstance(control, IceMakerControl)
options = [IceMakerMode.OFF.value, IceMakerMode.ON.value]
if control.has_max_ice:
options.append(IceMakerMode.MAX_ICE.value)
return options
def _hydro_breeze_options(control: SelectControl) -> list[str]:
"""Return available HydroBreeze options."""
return [mode.value for mode in HydroBreezeMode]
def _bio_fresh_plus_options(control: SelectControl) -> list[str]:
"""Return available BioFresh-Plus options."""
if TYPE_CHECKING:
assert isinstance(control, BioFreshPlusControl)
return [
mode.value
for mode in control.supported_modes
if isinstance(mode, BioFreshPlusMode)
]
SELECT_TYPES: list[LiebherrSelectEntityDescription] = [
LiebherrSelectEntityDescription(
key="ice_maker",
translation_key="ice_maker",
control_type=IceMakerControl,
mode_enum=IceMakerMode,
current_mode_fn=lambda c: c.ice_maker_mode, # type: ignore[union-attr]
options_fn=_ice_maker_options,
set_fn=lambda coordinator, zone_id, mode: coordinator.client.set_ice_maker(
device_id=coordinator.device_id,
zone_id=zone_id,
mode=mode, # type: ignore[arg-type]
),
),
LiebherrSelectEntityDescription(
key="hydro_breeze",
translation_key="hydro_breeze",
control_type=HydroBreezeControl,
mode_enum=HydroBreezeMode,
current_mode_fn=lambda c: c.current_mode, # type: ignore[union-attr]
options_fn=_hydro_breeze_options,
set_fn=lambda coordinator, zone_id, mode: coordinator.client.set_hydro_breeze(
device_id=coordinator.device_id,
zone_id=zone_id,
mode=mode, # type: ignore[arg-type]
),
),
LiebherrSelectEntityDescription(
key="bio_fresh_plus",
translation_key="bio_fresh_plus",
control_type=BioFreshPlusControl,
mode_enum=BioFreshPlusMode,
current_mode_fn=lambda c: c.current_mode, # type: ignore[union-attr]
options_fn=_bio_fresh_plus_options,
set_fn=lambda coordinator, zone_id, mode: coordinator.client.set_bio_fresh_plus(
device_id=coordinator.device_id,
zone_id=zone_id,
mode=mode, # type: ignore[arg-type]
),
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: LiebherrConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Liebherr select entities."""
entities: list[LiebherrSelectEntity] = []
for coordinator in entry.runtime_data.values():
has_multiple_zones = len(coordinator.data.get_temperature_controls()) > 1
for control in coordinator.data.controls:
for description in SELECT_TYPES:
if isinstance(control, description.control_type):
if TYPE_CHECKING:
assert isinstance(
control,
IceMakerControl | HydroBreezeControl | BioFreshPlusControl,
)
entities.append(
LiebherrSelectEntity(
coordinator=coordinator,
description=description,
zone_id=control.zone_id,
has_multiple_zones=has_multiple_zones,
)
)
async_add_entities(entities)
class LiebherrSelectEntity(LiebherrEntity, SelectEntity):
"""Representation of a Liebherr select entity."""
entity_description: LiebherrSelectEntityDescription
def __init__(
self,
coordinator: LiebherrCoordinator,
description: LiebherrSelectEntityDescription,
zone_id: int,
has_multiple_zones: bool,
) -> None:
"""Initialize the select entity."""
super().__init__(coordinator)
self.entity_description = description
self._zone_id = zone_id
self._attr_unique_id = f"{coordinator.device_id}_{description.key}_{zone_id}"
# Set options from the control
control = self._select_control
if control is not None:
self._attr_options = description.options_fn(control)
# Add zone suffix only for multi-zone devices
if has_multiple_zones:
temp_controls = coordinator.data.get_temperature_controls()
if (
(tc := temp_controls.get(zone_id))
and isinstance(tc.zone_position, ZonePosition)
and (zone_key := ZONE_POSITION_MAP.get(tc.zone_position))
):
self._attr_translation_key = f"{description.translation_key}_{zone_key}"
@property
def _select_control(self) -> SelectControl | None:
"""Get the select control for this entity."""
for control in self.coordinator.data.controls:
if (
isinstance(control, self.entity_description.control_type)
and control.zone_id == self._zone_id
):
if TYPE_CHECKING:
assert isinstance(
control,
IceMakerControl | HydroBreezeControl | BioFreshPlusControl,
)
return control
return None
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
control = self._select_control
if TYPE_CHECKING:
assert isinstance(
control,
IceMakerControl | HydroBreezeControl | BioFreshPlusControl,
)
mode = self.entity_description.current_mode_fn(control)
if isinstance(mode, StrEnum):
return mode.value
return None
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._select_control is not None
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
mode = self.entity_description.mode_enum(option)
await self._async_send_command(
self.entity_description.set_fn(self.coordinator, self._zone_id, mode),
)
@@ -47,6 +47,112 @@
"name": "Top zone setpoint"
}
},
"select": {
"bio_fresh_plus": {
"name": "BioFresh-Plus",
"state": {
"minus_two_minus_two": "-2°C | -2°C",
"minus_two_zero": "-2°C | 0°C",
"zero_minus_two": "0°C | -2°C",
"zero_zero": "0°C | 0°C"
}
},
"bio_fresh_plus_bottom_zone": {
"name": "Bottom zone BioFresh-Plus",
"state": {
"minus_two_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_minus_two%]",
"minus_two_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_zero%]",
"zero_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_minus_two%]",
"zero_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_zero%]"
}
},
"bio_fresh_plus_middle_zone": {
"name": "Middle zone BioFresh-Plus",
"state": {
"minus_two_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_minus_two%]",
"minus_two_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_zero%]",
"zero_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_minus_two%]",
"zero_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_zero%]"
}
},
"bio_fresh_plus_top_zone": {
"name": "Top zone BioFresh-Plus",
"state": {
"minus_two_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_minus_two%]",
"minus_two_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::minus_two_zero%]",
"zero_minus_two": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_minus_two%]",
"zero_zero": "[%key:component::liebherr::entity::select::bio_fresh_plus::state::zero_zero%]"
}
},
"hydro_breeze": {
"name": "HydroBreeze",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"off": "[%key:common::state::off%]"
}
},
"hydro_breeze_bottom_zone": {
"name": "Bottom zone HydroBreeze",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"off": "[%key:common::state::off%]"
}
},
"hydro_breeze_middle_zone": {
"name": "Middle zone HydroBreeze",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"off": "[%key:common::state::off%]"
}
},
"hydro_breeze_top_zone": {
"name": "Top zone HydroBreeze",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"off": "[%key:common::state::off%]"
}
},
"ice_maker": {
"name": "IceMaker",
"state": {
"max_ice": "MaxIce",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"ice_maker_bottom_zone": {
"name": "Bottom zone IceMaker",
"state": {
"max_ice": "[%key:component::liebherr::entity::select::ice_maker::state::max_ice%]",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"ice_maker_middle_zone": {
"name": "Middle zone IceMaker",
"state": {
"max_ice": "[%key:component::liebherr::entity::select::ice_maker::state::max_ice%]",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"ice_maker_top_zone": {
"name": "Top zone IceMaker",
"state": {
"max_ice": "[%key:component::liebherr::entity::select::ice_maker::state::max_ice%]",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
}
},
"sensor": {
"bottom_zone": {
"name": "Bottom zone"
+35
View File
@@ -6,9 +6,15 @@ from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from pyliebherrhomeapi import (
BioFreshPlusControl,
BioFreshPlusMode,
Device,
DeviceState,
DeviceType,
HydroBreezeControl,
HydroBreezeMode,
IceMakerControl,
IceMakerMode,
TemperatureControl,
TemperatureUnit,
ToggleControl,
@@ -83,6 +89,32 @@ MOCK_DEVICE_STATE = DeviceState(
zone_position=None,
value=True,
),
IceMakerControl(
name="icemaker",
type="IceMakerControl",
zone_id=2,
zone_position=ZonePosition.BOTTOM,
ice_maker_mode=IceMakerMode.OFF,
has_max_ice=True,
),
HydroBreezeControl(
name="hydrobreeze",
type="HydroBreezeControl",
zone_id=1,
current_mode=HydroBreezeMode.LOW,
),
BioFreshPlusControl(
name="biofreshplus",
type="BioFreshPlusControl",
zone_id=1,
current_mode=BioFreshPlusMode.ZERO_ZERO,
supported_modes=[
BioFreshPlusMode.ZERO_ZERO,
BioFreshPlusMode.ZERO_MINUS_TWO,
BioFreshPlusMode.MINUS_TWO_MINUS_TWO,
BioFreshPlusMode.MINUS_TWO_ZERO,
],
),
],
)
@@ -140,6 +172,9 @@ def mock_liebherr_client() -> Generator[MagicMock]:
client.set_super_frost = AsyncMock()
client.set_party_mode = AsyncMock()
client.set_night_mode = AsyncMock()
client.set_ice_maker = AsyncMock()
client.set_hydro_breeze = AsyncMock()
client.set_bio_fresh_plus = AsyncMock()
yield client
@@ -60,6 +60,33 @@
'zone_id': None,
'zone_position': None,
}),
dict({
'has_max_ice': True,
'ice_maker_mode': 'off',
'name': 'icemaker',
'type': 'IceMakerControl',
'zone_id': 2,
'zone_position': 'bottom',
}),
dict({
'current_mode': 'low',
'name': 'hydrobreeze',
'type': 'HydroBreezeControl',
'zone_id': 1,
}),
dict({
'current_mode': 'zero_zero',
'name': 'biofreshplus',
'supported_modes': list([
'zero_zero',
'zero_minus_two',
'minus_two_minus_two',
'minus_two_zero',
]),
'temperature_unit': None,
'type': 'BioFreshPlusControl',
'zone_id': 1,
}),
]),
'device': dict({
'device_id': 'test_device_id',
@@ -0,0 +1,305 @@
# serializer version: 1
# name: test_selects[select.test_fridge_bottom_zone_icemaker-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'on',
'max_ice',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.test_fridge_bottom_zone_icemaker',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Bottom zone IceMaker',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Bottom zone IceMaker',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ice_maker_bottom_zone',
'unique_id': 'test_device_id_ice_maker_2',
'unit_of_measurement': None,
})
# ---
# name: test_selects[select.test_fridge_bottom_zone_icemaker-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Fridge Bottom zone IceMaker',
'options': list([
'off',
'on',
'max_ice',
]),
}),
'context': <ANY>,
'entity_id': 'select.test_fridge_bottom_zone_icemaker',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_selects[select.test_fridge_top_zone_biofresh_plus-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'zero_zero',
'zero_minus_two',
'minus_two_minus_two',
'minus_two_zero',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.test_fridge_top_zone_biofresh_plus',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Top zone BioFresh-Plus',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Top zone BioFresh-Plus',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'bio_fresh_plus_top_zone',
'unique_id': 'test_device_id_bio_fresh_plus_1',
'unit_of_measurement': None,
})
# ---
# name: test_selects[select.test_fridge_top_zone_biofresh_plus-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Fridge Top zone BioFresh-Plus',
'options': list([
'zero_zero',
'zero_minus_two',
'minus_two_minus_two',
'minus_two_zero',
]),
}),
'context': <ANY>,
'entity_id': 'select.test_fridge_top_zone_biofresh_plus',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'zero_zero',
})
# ---
# name: test_selects[select.test_fridge_top_zone_hydrobreeze-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'low',
'medium',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.test_fridge_top_zone_hydrobreeze',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Top zone HydroBreeze',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Top zone HydroBreeze',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hydro_breeze_top_zone',
'unique_id': 'test_device_id_hydro_breeze_1',
'unit_of_measurement': None,
})
# ---
# name: test_selects[select.test_fridge_top_zone_hydrobreeze-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Fridge Top zone HydroBreeze',
'options': list([
'off',
'low',
'medium',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.test_fridge_top_zone_hydrobreeze',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'low',
})
# ---
# name: test_single_zone_select[select.single_zone_fridge_hydrobreeze-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'low',
'medium',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.single_zone_fridge_hydrobreeze',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'HydroBreeze',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'HydroBreeze',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hydro_breeze',
'unique_id': 'single_zone_id_hydro_breeze_1',
'unit_of_measurement': None,
})
# ---
# name: test_single_zone_select[select.single_zone_fridge_hydrobreeze-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Single Zone Fridge HydroBreeze',
'options': list([
'off',
'low',
'medium',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.single_zone_fridge_hydrobreeze',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_single_zone_select[select.single_zone_fridge_icemaker-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'on',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.single_zone_fridge_icemaker',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'IceMaker',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'IceMaker',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ice_maker',
'unique_id': 'single_zone_id_ice_maker_1',
'unit_of_measurement': None,
})
# ---
# name: test_single_zone_select[select.single_zone_fridge_icemaker-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Single Zone Fridge IceMaker',
'options': list([
'off',
'on',
]),
}),
'context': <ANY>,
'entity_id': 'select.single_zone_fridge_icemaker',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
+404
View File
@@ -0,0 +1,404 @@
"""Test the Liebherr select platform."""
import copy
from datetime import timedelta
from typing import Any
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pyliebherrhomeapi import (
BioFreshPlusMode,
Device,
DeviceState,
DeviceType,
HydroBreezeControl,
HydroBreezeMode,
IceMakerControl,
IceMakerMode,
TemperatureControl,
TemperatureUnit,
ZonePosition,
)
from pyliebherrhomeapi.exceptions import LiebherrConnectionError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
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 tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.SELECT]
@pytest.fixture(autouse=True)
def enable_all_entities(entity_registry_enabled_by_default: None) -> None:
"""Make sure all entities are enabled."""
@pytest.mark.usefixtures("init_integration")
async def test_selects(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test all select entities with multi-zone device."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "option", "method", "kwargs"),
[
(
"select.test_fridge_bottom_zone_icemaker",
"on",
"set_ice_maker",
{
"device_id": "test_device_id",
"zone_id": 2,
"mode": IceMakerMode.ON,
},
),
(
"select.test_fridge_bottom_zone_icemaker",
"max_ice",
"set_ice_maker",
{
"device_id": "test_device_id",
"zone_id": 2,
"mode": IceMakerMode.MAX_ICE,
},
),
(
"select.test_fridge_top_zone_hydrobreeze",
"high",
"set_hydro_breeze",
{
"device_id": "test_device_id",
"zone_id": 1,
"mode": HydroBreezeMode.HIGH,
},
),
(
"select.test_fridge_top_zone_hydrobreeze",
"off",
"set_hydro_breeze",
{
"device_id": "test_device_id",
"zone_id": 1,
"mode": HydroBreezeMode.OFF,
},
),
(
"select.test_fridge_top_zone_biofresh_plus",
"zero_minus_two",
"set_bio_fresh_plus",
{
"device_id": "test_device_id",
"zone_id": 1,
"mode": BioFreshPlusMode.ZERO_MINUS_TWO,
},
),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_select_service_calls(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
entity_id: str,
option: str,
method: str,
kwargs: dict[str, Any],
) -> None:
"""Test select option service calls."""
initial_call_count = mock_liebherr_client.get_device_state.call_count
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option},
blocking=True,
)
getattr(mock_liebherr_client, method).assert_called_once_with(**kwargs)
# Verify coordinator refresh was triggered
assert mock_liebherr_client.get_device_state.call_count > initial_call_count
@pytest.mark.parametrize(
("entity_id", "method", "option"),
[
("select.test_fridge_bottom_zone_icemaker", "set_ice_maker", "off"),
("select.test_fridge_top_zone_hydrobreeze", "set_hydro_breeze", "off"),
(
"select.test_fridge_top_zone_biofresh_plus",
"set_bio_fresh_plus",
"zero_zero",
),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_select_failure(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
entity_id: str,
method: str,
option: str,
) -> None:
"""Test select fails gracefully on connection error."""
getattr(mock_liebherr_client, method).side_effect = LiebherrConnectionError(
"Connection failed"
)
with pytest.raises(
HomeAssistantError,
match="An error occurred while communicating with the device",
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option},
blocking=True,
)
@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,
mock_liebherr_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test select entity behavior when control is removed."""
entity_id = "select.test_fridge_bottom_zone_icemaker"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "off"
# Device stops reporting select controls
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: DeviceState(
device=MOCK_DEVICE, controls=[]
)
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
async def test_single_zone_select(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_liebherr_client: MagicMock,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test single zone device uses name without zone suffix."""
device = Device(
device_id="single_zone_id",
nickname="Single Zone Fridge",
device_type=DeviceType.FRIDGE,
device_name="K2601",
)
mock_liebherr_client.get_devices.return_value = [device]
single_zone_state = DeviceState(
device=device,
controls=[
TemperatureControl(
zone_id=1,
zone_position=ZonePosition.TOP,
name="Fridge",
type="fridge",
value=4,
target=4,
min=2,
max=8,
unit=TemperatureUnit.CELSIUS,
),
IceMakerControl(
name="icemaker",
type="IceMakerControl",
zone_id=1,
zone_position=ZonePosition.TOP,
ice_maker_mode=IceMakerMode.ON,
has_max_ice=False,
),
HydroBreezeControl(
name="hydrobreeze",
type="HydroBreezeControl",
zone_id=1,
current_mode=HydroBreezeMode.OFF,
),
],
)
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy(
single_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()
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,
mock_liebherr_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test select entity state when control mode returns None."""
entity_id = "select.test_fridge_top_zone_hydrobreeze"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "low"
# Simulate update where mode is None
state_with_none_mode = copy.deepcopy(MOCK_DEVICE_STATE)
for control in state_with_none_mode.controls:
if isinstance(control, HydroBreezeControl):
control.current_mode = None
break
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: copy.deepcopy(
state_with_none_mode
)
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_UNKNOWN