diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 37e753c27a3..d30af851e7d 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -118,9 +118,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): await self.async_update_color_zones() if lifx_features(self.device)["hev"]: - if self.device.hev_cycle_configuration is None: - self.device.get_hev_configuration() - await self.async_get_hev_cycle() async def async_update_color_zones(self) -> None: @@ -195,3 +192,10 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): apply=apply, ) ) + + async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None: + """Start or stop an HEV cycle on a LIFX Clean bulb.""" + if lifx_features(self.device)["hev"]: + await async_execute_lifx( + partial(self.device.set_hev_cycle, enable=enable, duration=duration) + ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 36d3b480f74..4df04f2d1e7 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -28,7 +28,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util -from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN +from .const import ( + ATTR_DURATION, + ATTR_INFRARED, + ATTR_POWER, + ATTR_ZONES, + DATA_LIFX_MANAGER, + DOMAIN, +) from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( @@ -43,14 +50,20 @@ LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" -LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( - { - **LIGHT_TURN_ON_SCHEMA, - ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), - ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), - ATTR_POWER: cv.boolean, - } -) +LIFX_SET_STATE_SCHEMA = { + **LIGHT_TURN_ON_SCHEMA, + ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), + ATTR_POWER: cv.boolean, +} + + +SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state" + +LIFX_SET_HEV_CYCLE_STATE_SCHEMA = { + ATTR_POWER: vol.Required(cv.boolean), + ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)), +} HSBK_HUE = 0 HSBK_SATURATION = 1 @@ -74,6 +87,11 @@ async def async_setup_entry( LIFX_SET_STATE_SCHEMA, "set_state", ) + platform.async_register_entity_service( + SERVICE_LIFX_SET_HEV_CYCLE_STATE, + LIFX_SET_HEV_CYCLE_STATE_SCHEMA, + "set_hev_cycle_state", + ) if lifx_features(device)["multizone"]: entity: LIFXLight = LIFXStrip(coordinator, manager, entry) elif lifx_features(device)["color"]: @@ -237,6 +255,18 @@ class LIFXLight(LIFXEntity, LightEntity): # Update when the transition starts and ends await self.update_during_transition(fade) + async def set_hev_cycle_state( + self, power: bool, duration: int | None = None + ) -> None: + """Set the state of the HEV LEDs on a LIFX Clean bulb.""" + if lifx_features(self.bulb)["hev"] is False: + raise HomeAssistantError( + "This device does not support setting HEV cycle state" + ) + + await self.coordinator.async_set_hev_cycle_state(power, duration or 0) + await self.update_during_transition(duration or 0) + async def set_power( self, pwr: bool, diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e499ad1b3b8..5208be89638 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -1,3 +1,29 @@ +set_hev_cycle_state: + name: Set HEV cycle state + description: Control the HEV LEDs on a LIFX Clean bulb. + target: + entity: + integration: lifx + domain: light + fields: + power: + name: enable + description: Start or stop a Clean cycle. + required: true + example: true + selector: + boolean: + duration: + name: Duration + description: How long the HEV LEDs will remain on. Uses the configured default duration if not specified. + required: false + default: 7200 + example: 3600 + selector: + number: + min: 0 + max: 86400 + unit_of_measurement: seconds set_state: name: Set State description: Set a color/brightness and possibly turn the light on/off. diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 9e137c8532a..05d7e9a1ddf 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -118,9 +118,9 @@ def _mocked_brightness_bulb() -> Light: def _mocked_clean_bulb() -> Light: bulb = _mocked_bulb() - bulb.get_hev_cycle = MockLifxCommand( - bulb, duration=7200, remaining=0, last_power=False - ) + bulb.get_hev_cycle = MockLifxCommand(bulb) + bulb.set_hev_cycle = MockLifxCommand(bulb) + bulb.hev_cycle_configuration = {"duration": 7200, "indication": False} bulb.hev_cycle = { "duration": 7200, "remaining": 30, diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6229e130a40..6555e483f5f 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components import lifx from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.const import ATTR_POWER from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP from homeassistant.components.light import ( @@ -40,6 +41,7 @@ from . import ( _mocked_brightness_bulb, _mocked_bulb, _mocked_bulb_new_firmware, + _mocked_clean_bulb, _mocked_light_strip, _mocked_white_bulb, _patch_config_flow_try_connect, @@ -997,3 +999,61 @@ async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: ) assert bulb.set_color.calls[0][0][0] == [0, 0, 25700, 3500] assert len(bulb.set_power.calls) == 1 + + +async def test_clean_bulb(hass: HomeAssistant) -> None: + """Test setting HEV cycle state on Clean bulbs.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_clean_bulb() + bulb.power_level = 0 + bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + state = hass.states.get(entity_id) + assert state.state == "off" + await hass.services.async_call( + DOMAIN, + "set_hev_cycle_state", + {ATTR_ENTITY_ID: entity_id, ATTR_POWER: True}, + blocking=True, + ) + + call_dict = bulb.set_hev_cycle.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 0, "enable": True} + bulb.set_hev_cycle.reset_mock() + + +async def test_set_hev_cycle_state_fails_for_color_bulb(hass: HomeAssistant) -> None: + """Test that set_hev_cycle_state fails for a non-Clean bulb.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 0 + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + state = hass.states.get(entity_id) + assert state.state == "off" + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_hev_cycle_state", + {ATTR_ENTITY_ID: entity_id, ATTR_POWER: True}, + blocking=True, + )