diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 13247c42034..d6348d0eb7e 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -18,6 +18,7 @@ from .const import DOMAIN from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator PLATFORMS: list[Platform] = [ + Platform.LIGHT, Platform.SENSOR, ] diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 86239ee6590..bd9cd1e6100 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -9,6 +9,10 @@ ACTIONS = "actions" POWER_ON = "powerOn" POWER_OFF = "powerOff" PROCESS_ACTION = "processAction" +AMBIENT_LIGHT = "ambientLight" +LIGHT = "light" +LIGHT_ON = 1 +LIGHT_OFF = 2 class MieleAppliance(IntEnum): diff --git a/homeassistant/components/miele/light.py b/homeassistant/components/miele/light.py new file mode 100644 index 00000000000..46d94e65511 --- /dev/null +++ b/homeassistant/components/miele/light.py @@ -0,0 +1,140 @@ +"""Platform for Miele light entity.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any, Final + +import aiohttp + +from homeassistant.components.light import ( + ColorMode, + LightEntity, + LightEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import AMBIENT_LIGHT, DOMAIN, LIGHT, LIGHT_OFF, LIGHT_ON, MieleAppliance +from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator +from .entity import MieleDevice, MieleEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class MieleLightDescription(LightEntityDescription): + """Class describing Miele light entities.""" + + value_fn: Callable[[MieleDevice], StateType] + light_type: str + + +@dataclass +class MieleLightDefinition: + """Class for defining light entities.""" + + types: tuple[MieleAppliance, ...] + description: MieleLightDescription + + +LIGHT_TYPES: Final[tuple[MieleLightDefinition, ...]] = ( + MieleLightDefinition( + types=( + MieleAppliance.OVEN, + MieleAppliance.OVEN_MICROWAVE, + MieleAppliance.STEAM_OVEN, + MieleAppliance.MICROWAVE, + MieleAppliance.COFFEE_SYSTEM, + MieleAppliance.HOOD, + MieleAppliance.STEAM_OVEN_COMBI, + MieleAppliance.WINE_CABINET, + MieleAppliance.WINE_CONDITIONING_UNIT, + MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT, + MieleAppliance.STEAM_OVEN_MICRO, + MieleAppliance.WINE_CABINET_FREEZER, + MieleAppliance.STEAM_OVEN_MK2, + ), + description=MieleLightDescription( + key="light", + value_fn=lambda value: value.state_light, + light_type=LIGHT, + translation_key="light", + ), + ), + MieleLightDefinition( + types=(MieleAppliance.HOOD,), + description=MieleLightDescription( + key="ambient_light", + value_fn=lambda value: value.state_ambient_light, + light_type=AMBIENT_LIGHT, + translation_key="ambient_light", + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: MieleConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the light platform.""" + coordinator = config_entry.runtime_data + + async_add_entities( + MieleLight(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in LIGHT_TYPES + if device.device_type in definition.types + ) + + +class MieleLight(MieleEntity, LightEntity): + """Representation of a Light.""" + + entity_description: MieleLightDescription + _attr_color_mode = ColorMode.ONOFF + _attr_supported_color_modes = {ColorMode.ONOFF} + + def __init__( + self, + coordinator: MieleDataUpdateCoordinator, + device_id: str, + description: MieleLightDescription, + ) -> None: + """Initialize the light.""" + super().__init__(coordinator, device_id, description) + self.api = coordinator.api + + @property + def is_on(self) -> bool: + """Return current on/off state.""" + return self.entity_description.value_fn(self.device) == LIGHT_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + await self.async_turn_light(LIGHT_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.async_turn_light(LIGHT_OFF) + + async def async_turn_light(self, mode: int) -> None: + """Set light to mode.""" + try: + await self.api.send_action( + self._device_id, {self.entity_description.light_type: mode} + ) + except aiohttp.ClientError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_state_error", + translation_placeholders={ + "entity": self.entity_id, + }, + ) from err diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index a25d0613a81..dcf2e270ffd 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -114,6 +114,14 @@ } }, "entity": { + "light": { + "ambient_light": { + "name": "Ambient light" + }, + "light": { + "name": "[%key:component::light::title%]" + } + }, "sensor": { "status": { "name": "Status", @@ -147,7 +155,7 @@ "config_entry_not_ready": { "message": "Error while loading the integration." }, - "set_switch_error": { + "set_state_error": { "message": "Failed to set state for {entity}." } } diff --git a/tests/components/miele/snapshots/test_light.ambr b/tests/components/miele/snapshots/test_light.ambr new file mode 100644 index 00000000000..128b642d7a0 --- /dev/null +++ b/tests/components/miele/snapshots/test_light.ambr @@ -0,0 +1,113 @@ +# serializer version: 1 +# name: test_light_states[platforms0][light.hood_ambient_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hood_ambient_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ambient light', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ambient_light', + 'unique_id': 'DummyAppliance_18-ambient_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_states[platforms0][light.hood_ambient_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Hood Ambient light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hood_ambient_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_light_states[platforms0][light.hood_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hood_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'DummyAppliance_18-light', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_states[platforms0][light.hood_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Hood Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hood_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/miele/test_light.py b/tests/components/miele/test_light.py new file mode 100644 index 00000000000..286c2df0dd8 --- /dev/null +++ b/tests/components/miele/test_light.py @@ -0,0 +1,80 @@ +"""Tests for miele light module.""" + +from unittest.mock import MagicMock + +from aiohttp import ClientError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +TEST_PLATFORM = LIGHT_DOMAIN +pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) + +ENTITY_ID = "light.hood_light" + + +async def test_light_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test light entity state.""" + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("service", "light_state"), + [ + (SERVICE_TURN_ON, 1), + (SERVICE_TURN_OFF, 2), + ], +) +async def test_light_toggle( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + service: str, + light_state: int, +) -> None: + """Test the light can be turned on/off.""" + + await hass.services.async_call( + TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + mock_miele_client.send_action.assert_called_once_with( + "DummyAppliance_18", {"light": light_state} + ) + + +@pytest.mark.parametrize( + ("service"), + [ + (SERVICE_TURN_ON), + (SERVICE_TURN_OFF), + ], +) +async def test_api_failure( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + service: str, +) -> None: + """Test handling of exception from API.""" + mock_miele_client.send_action.side_effect = ClientError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + mock_miele_client.send_action.assert_called_once()