diff --git a/homeassistant/components/lunatone/light.py b/homeassistant/components/lunatone/light.py index 416412aea6e..16f2c5384ce 100644 --- a/homeassistant/components/lunatone/light.py +++ b/homeassistant/components/lunatone/light.py @@ -5,11 +5,17 @@ from __future__ import annotations import asyncio from typing import Any -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + brightness_supported, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.color import brightness_to_value, value_to_brightness from .const import DOMAIN from .coordinator import LunatoneConfigEntry, LunatoneDevicesDataUpdateCoordinator @@ -42,8 +48,10 @@ class LunatoneLight( ): """Representation of a Lunatone light.""" - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} + BRIGHTNESS_SCALE = (1, 100) + + _last_brightness = 255 + _attr_has_entity_name = True _attr_name = None _attr_should_poll = False @@ -82,6 +90,25 @@ class LunatoneLight( """Return True if light is on.""" return self._device is not None and self._device.is_on + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + if self._device is None: + return 0 + return value_to_brightness(self.BRIGHTNESS_SCALE, self._device.brightness) + + @property + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + if self._device is not None and self._device.is_dimmable: + return ColorMode.BRIGHTNESS + return ColorMode.ONOFF + + @property + def supported_color_modes(self) -> set[ColorMode]: + """Return the supported color modes.""" + return {self.color_mode} + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -91,13 +118,27 @@ class LunatoneLight( async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" assert self._device - await self._device.switch_on() + + if brightness_supported(self.supported_color_modes): + brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) + await self._device.fade_to_brightness( + brightness_to_value(self.BRIGHTNESS_SCALE, brightness) + ) + else: + await self._device.switch_on() + await asyncio.sleep(STATUS_UPDATE_DELAY) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" assert self._device - await self._device.switch_off() + + if brightness_supported(self.supported_color_modes): + self._last_brightness = self.brightness + await self._device.fade_to_brightness(0) + else: + await self._device.switch_off() + await asyncio.sleep(STATUS_UPDATE_DELAY) await self.coordinator.async_refresh() diff --git a/tests/components/lunatone/conftest.py b/tests/components/lunatone/conftest.py index 21a80891dd9..89e3adfc0bf 100644 --- a/tests/components/lunatone/conftest.py +++ b/tests/components/lunatone/conftest.py @@ -27,6 +27,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def mock_lunatone_devices() -> Generator[AsyncMock]: """Mock a Lunatone devices object.""" + state = {"is_dimmable": False} def build_devices_mock(devices: Devices): device_list = [] @@ -36,6 +37,10 @@ def mock_lunatone_devices() -> Generator[AsyncMock]: device.id = device.data.id device.name = device.data.name device.is_on = device.data.features.switchable.status + device.brightness = device.data.features.dimmable.status + type(device).is_dimmable = PropertyMock( + side_effect=lambda s=state: s["is_dimmable"] + ) device_list.append(device) return device_list @@ -47,6 +52,7 @@ def mock_lunatone_devices() -> Generator[AsyncMock]: type(devices).devices = PropertyMock( side_effect=lambda d=devices: build_devices_mock(d) ) + devices.set_is_dimmable = lambda value, s=state: s.update(is_dimmable=value) yield devices diff --git a/tests/components/lunatone/test_light.py b/tests/components/lunatone/test_light.py index 64262ad497b..24065d10049 100644 --- a/tests/components/lunatone/test_light.py +++ b/tests/components/lunatone/test_light.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -47,7 +47,6 @@ async def test_turn_on_off( mock_lunatone_devices: AsyncMock, mock_lunatone_info: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, ) -> None: """Test the light can be turned on and off.""" await setup_integration(hass, mock_config_entry) @@ -77,3 +76,59 @@ async def test_turn_on_off( state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_turn_on_off_with_brightness( + hass: HomeAssistant, + mock_lunatone_devices: AsyncMock, + mock_lunatone_info: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the light can be turned on with brightness.""" + expected_brightness = 128 + brightness_percentages = iter([50.0, 0.0, 50.0]) + + mock_lunatone_devices.set_is_dimmable(True) + + await setup_integration(hass, mock_config_entry) + + async def fake_update(): + brightness = next(brightness_percentages) + device = mock_lunatone_devices.data.devices[0] + device.features.switchable.status = brightness > 0 + device.features.dimmable.status = brightness + + mock_lunatone_devices.async_update.side_effect = fake_update + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: expected_brightness}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + assert state.attributes["brightness"] == expected_brightness + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + assert not state.attributes["brightness"] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + assert state.attributes["brightness"] == expected_brightness