diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 11b6a4812e8..640502584f8 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -24,11 +24,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_WHITE_VALUE ) @@ -81,10 +77,13 @@ class DemoLight(LightEntity): self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness self._white = white + self._features = SUPPORT_DEMO self._effect_list = effect_list self._effect = effect self._available = True self._color_mode = "ct" if ct is not None and hs_color is None else "hs" + if self._effect_list is not None: + self._features |= SUPPORT_EFFECT @property def device_info(self): @@ -161,7 +160,7 @@ class DemoLight(LightEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORT_DEMO + return self._features async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 6ff19aedeb4..b347294f275 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1280,6 +1280,9 @@ class ModesTrait(_Trait): if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES: return True + if domain == light.DOMAIN and features & light.SUPPORT_EFFECT: + return True + if domain != media_player.DOMAIN: return False @@ -1317,6 +1320,7 @@ class ModesTrait(_Trait): (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"), (input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"), (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"), + (light.DOMAIN, light.ATTR_EFFECT_LIST, "effect"), ): if self.state.domain != domain: continue @@ -1347,6 +1351,9 @@ class ModesTrait(_Trait): elif self.state.domain == humidifier.DOMAIN: if humidifier.ATTR_MODE in attrs: mode_settings["mode"] = attrs.get(humidifier.ATTR_MODE) + elif self.state.domain == light.DOMAIN: + if light.ATTR_EFFECT in attrs: + mode_settings["effect"] = attrs.get(light.ATTR_EFFECT) if mode_settings: response["on"] = self.state.state not in (STATE_OFF, STATE_UNKNOWN) @@ -1386,6 +1393,20 @@ class ModesTrait(_Trait): ) return + if self.state.domain == light.DOMAIN: + requested_effect = settings["effect"] + await self.hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: self.state.entity_id, + light.ATTR_EFFECT: requested_effect, + }, + blocking=True, + context=data.context, + ) + return + if self.state.domain != media_player.DOMAIN: _LOGGER.info( "Received an Options command for unrecognised domain %s", diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f665fa53ed2..f4e26a77f48 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -128,6 +128,7 @@ DEMO_DEVICES = [ "action.devices.traits.OnOff", "action.devices.traits.Brightness", "action.devices.traits.ColorSetting", + "action.devices.traits.Modes", ], "type": "action.devices.types.LIGHT", "willReportState": False, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6cd99d1fdd1..db97a42dff4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -9,7 +9,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover -from homeassistant.components.demo.light import DemoLight +from homeassistant.components.demo.light import LIGHT_EFFECT_LIST, DemoLight from homeassistant.components.demo.media_player import AbstractDemoPlayer from homeassistant.components.demo.switch import DemoSwitch from homeassistant.components.google_assistant import ( @@ -48,7 +48,14 @@ def registries(hass): async def test_sync_message(hass): """Test a sync message.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" await light.async_update_ha_state() @@ -95,10 +102,37 @@ async def test_sync_message(hass): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, @@ -132,7 +166,14 @@ async def test_sync_in_area(hass, registries): "light", "test", "1235", suggested_object_id="demo_light", device_id=device.id ) - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = entity.entity_id await light.async_update_ha_state() @@ -162,10 +203,37 @@ async def test_sync_in_area(hass, registries): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, @@ -186,7 +254,14 @@ async def test_sync_in_area(hass, registries): async def test_query_message(hass): """Test a sync message.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" await light.async_update_ha_state() @@ -555,7 +630,14 @@ async def test_serialize_input_boolean(hass): async def test_unavailable_state_does_sync(hass): """Test that an unavailable entity does sync over.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" light._available = False # pylint: disable=protected-access @@ -584,10 +666,37 @@ async def test_unavailable_state_does_sync(hass): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 54a8acf1a65..9d821b357ca 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -515,6 +515,73 @@ async def test_color_light_temperature_light_bad_temp(hass): assert trt.query_attributes() == {} +async def test_light_modes(hass): + """Test Light Mode trait.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None + assert trait.ModesTrait.supported(light.DOMAIN, light.SUPPORT_EFFECT, None) + + trt = trait.ModesTrait( + hass, + State( + "light.living_room", + light.STATE_ON, + attributes={ + light.ATTR_EFFECT_LIST: ["random", "colorloop"], + light.ATTR_EFFECT: "random", + }, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "effect", + "name_values": [{"name_synonym": ["effect"], "lang": "en"}], + "settings": [ + { + "setting_name": "random", + "setting_values": [ + {"setting_synonym": ["random"], "lang": "en"} + ], + }, + { + "setting_name": "colorloop", + "setting_values": [ + {"setting_synonym": ["colorloop"], "lang": "en"} + ], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"effect": "random"}, + "on": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, params={"updateModeSettings": {"effect": "colorloop"}}, + ) + + calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) + await trt.execute( + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"effect": "colorloop"}}, + {}, + ) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "light.living_room", + "effect": "colorloop", + } + + async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" assert helpers.get_google_type(scene.DOMAIN, None) is not None diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 2a2e21f77c5..685db475b8c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -545,13 +545,11 @@ async def test_service_calls(hass): state = hass.states.get("light.ceiling_lights") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 128 - assert state.attributes[ATTR_EFFECT] == "Random" assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) state = hass.states.get("light.kitchen_lights") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 128 - assert state.attributes[ATTR_EFFECT] == "Random" assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255)