From 72d7817dbf10f7c378558780d226c2655410ce70 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:51:14 +0200 Subject: [PATCH] Update light turn_on schema to coerce colors to tuple before asserting sequence type (#58670) * Make color_name_to_rgb return a tuple * Tweak * Tweak * Update test * Tweak test --- homeassistant/components/group/light.py | 5 + homeassistant/components/light/__init__.py | 10 +- tests/components/group/test_light.py | 189 ++++++++++++++++++++- tests/components/light/test_init.py | 78 +++++++++ 4 files changed, 273 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index a3a02ee6b9c..4a14bc5dcf3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import Counter import itertools +import logging from typing import Any, Set, cast import voluptuous as vol @@ -66,6 +67,8 @@ SUPPORT_GROUP_LIGHT = ( SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -152,6 +155,8 @@ class LightGroup(GroupEntity, light.LightEntity): } data[ATTR_ENTITY_ID] = self._entity_ids + _LOGGER.debug("Forwarded turn_on command: %s", data) + await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8b1d8fe1ec6..c5ae88eaaa0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -202,25 +202,25 @@ LIGHT_TURN_ON_SCHEMA = { ), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) ), vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 4) ), vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 5) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index e769bf33f8a..e1a45d6fe53 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -3,12 +3,15 @@ from os import path import unittest.mock from unittest.mock import MagicMock, patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -28,6 +31,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, + COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE, @@ -261,6 +265,77 @@ async def test_color_hs(hass, enable_custom_integrations): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 +async def test_color_rgb(hass, enable_custom_integrations): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGB} + entity0.color_mode = COLOR_MODE_RGB + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGB} + entity1.color_mode = COLOR_MODE_RGB + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (0, 64, 128) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (127, 96, 96) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (255, 128, 64) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + async def test_color_rgbw(hass, enable_custom_integrations): """Test rgbw color reporting.""" platform = getattr(hass.components, "test.light") @@ -1039,14 +1114,40 @@ async def test_supported_features(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 -async def test_service_calls(hass): +@pytest.mark.parametrize("supported_color_modes", [COLOR_MODE_HS, COLOR_MODE_RGB]) +async def test_service_calls(hass, enable_custom_integrations, supported_color_modes): """Test service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("bed_light", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("ceiling_lights", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("kitchen_lights", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {supported_color_modes} + entity0.color_mode = supported_color_modes + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {supported_color_modes} + entity1.color_mode = supported_color_modes + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {supported_color_modes} + entity2.color_mode = supported_color_modes + entity2.brightness = 255 + entity2.rgb_color = (255, 128, 64) + await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, "entities": [ @@ -1062,14 +1163,16 @@ async def test_service_calls(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON + group_state = hass.states.get("light.light_group") + assert group_state.state == STATE_ON + assert group_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [supported_color_modes] + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "light.light_group"}, blocking=True, ) - assert hass.states.get("light.bed_light").state == STATE_OFF assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF @@ -1096,6 +1199,84 @@ async def test_service_calls(hass): assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_RGB_COLOR: (42, 255, 255), + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + 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_RGB_COLOR] == (42, 255, 255) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_COLOR_NAME: "red", + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.kitchen_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + +async def test_service_call_effect(hass): + """Test service calls.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.light_group").state == STATE_ON + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 2a61a3bbf4d..d51a5b64861 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component +import homeassistant.util.color as color_util from tests.common import async_mock_service @@ -1724,6 +1725,83 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} +async def test_light_service_call_color_conversion_named_tuple( + hass, enable_custom_integrations +): + """Test a named tuple (RGBColor) is handled correctly.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGB} + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_XY} + + entity3 = platform.ENTITIES[3] + entity3.supported_color_modes = { + light.COLOR_MODE_HS, + light.COLOR_MODE_RGB, + light.COLOR_MODE_XY, + } + + entity4 = platform.ENTITIES[4] + entity4.supported_features = light.SUPPORT_COLOR + + entity5 = platform.ENTITIES[5] + entity5.supported_color_modes = {light.COLOR_MODE_RGBW} + + entity6 = platform.ENTITIES[6] + entity6.supported_color_modes = {light.COLOR_MODE_RGBWW} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 25, + "rgb_color": color_util.RGBColor(128, 0, 0), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 64, "xy_color": (0.701, 0.299)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} + + async def test_light_service_call_color_temp_emulation( hass, enable_custom_integrations ):