From d4bd4c114b0430f50281df06ef46d007bb773a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Thu, 16 Nov 2017 08:00:43 +0100 Subject: [PATCH] add support for color temperature and color to Google Assistant (#10039) * add support for color temperature and color; also add some extra deviceInfo attributes * change so that default behaviour doesn't turn off device if the action isn't handled * add tests * fix lint * more lint * use attributes were applicable * removed debug logging * fix unassigned if only None returned * report more data in QUERY * better tests for color and temperature * fixes after dev merge * remove deviceInfo as not part of a device state (PR #10399) * fix after merge --- .../components/google_assistant/http.py | 1 + .../components/google_assistant/smart_home.py | 73 +++++++++++++++++-- .../google_assistant/test_google_assistant.py | 36 ++++++++- .../google_assistant/test_smart_home.py | 51 +++++++++++++ 4 files changed, 154 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 71a4ff9ce3a..ab9705432fb 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -128,6 +128,7 @@ class GoogleAssistantView(HomeAssistantView): ent_ids = [ent.get('id') for ent in command.get('devices', [])] execution = command.get('execution')[0] for eid in ent_ids: + success = False domain = eid.split('.')[0] (service, service_data) = determine_service( eid, execution.get('command'), execution.get('params'), diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 42cb555fe3c..cd1583fb377 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,6 +8,7 @@ from aiohttp.web import Request, Response # NOQA from typing import Dict, Tuple, Any, Optional # NOQA from homeassistant.helpers.entity import Entity # NOQA from homeassistant.core import HomeAssistant # NOQA +from homeassistant.util import color from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.const import ( @@ -22,7 +23,8 @@ from homeassistant.components import ( from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( - ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE, + ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR, + ATTR_GOOGLE_ASSISTANT_TYPE, COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE, COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE, @@ -78,6 +80,7 @@ def entity_to_device(entity: Entity, units: UnitSystem): device = { 'id': entity.entity_id, 'name': {}, + 'attributes': {}, 'traits': [], 'willReportState': False, } @@ -102,6 +105,23 @@ def entity_to_device(entity: Entity, units: UnitSystem): for feature, trait in class_data[2].items(): if feature & supported > 0: device['traits'].append(trait) + + # Actions require this attributes for a device + # supporting temperature + # For IKEA trådfri, these attributes only seem to + # be set only if the device is on? + if trait == TRAIT_COLOR_TEMP: + if entity.attributes.get( + light.ATTR_MAX_MIREDS) is not None: + device['attributes']['temperatureMinK'] = \ + int(round(color.color_temperature_mired_to_kelvin( + entity.attributes.get(light.ATTR_MAX_MIREDS)))) + if entity.attributes.get( + light.ATTR_MIN_MIREDS) is not None: + device['attributes']['temperatureMaxK'] = \ + int(round(color.color_temperature_mired_to_kelvin( + entity.attributes.get(light.ATTR_MIN_MIREDS)))) + if entity.domain == climate.DOMAIN: modes = ','.join( m for m in entity.attributes.get(climate.ATTR_OPERATION_LIST, []) @@ -156,12 +176,35 @@ def query_device(entity: Entity, units: UnitSystem) -> dict: final_brightness = 100 * (final_brightness / 255) - return { + query_response = { "on": final_state, "online": True, "brightness": int(final_brightness) } + supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported_features & \ + (light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR): + query_response["color"] = {} + + if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None: + query_response["color"]["temperature"] = \ + int(round(color.color_temperature_mired_to_kelvin( + entity.attributes.get(light.ATTR_COLOR_TEMP)))) + + if entity.attributes.get(light.ATTR_COLOR_NAME) is not None: + query_response["color"]["name"] = \ + entity.attributes.get(light.ATTR_COLOR_NAME) + + if entity.attributes.get(light.ATTR_RGB_COLOR) is not None: + color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR) + if color_rgb is not None: + query_response["color"]["spectrumRGB"] = \ + int(color.color_rgb_to_hex( + color_rgb[0], color_rgb[1], color_rgb[2]), 16) + + return query_response + # erroneous bug on old pythons and pylint # https://github.com/PyCQA/pylint/issues/1212 @@ -217,7 +260,27 @@ def determine_service( service_data['brightness'] = int(brightness / 100 * 255) return (SERVICE_TURN_ON, service_data) - if command == COMMAND_ACTIVATESCENE or (COMMAND_ONOFF == command and - params.get('on') is True): + _LOGGER.debug("Handling command %s with data %s", command, params) + if command == COMMAND_COLOR: + color_data = params.get('color') + if color_data is not None: + if color_data.get('temperature', 0) > 0: + service_data[light.ATTR_KELVIN] = color_data.get('temperature') + return (SERVICE_TURN_ON, service_data) + if color_data.get('spectrumRGB', 0) > 0: + # blue is 255 so pad up to 6 chars + hex_value = \ + ('%0x' % int(color_data.get('spectrumRGB'))).zfill(6) + service_data[light.ATTR_RGB_COLOR] = \ + color.rgb_hex_to_rgb_list(hex_value) + return (SERVICE_TURN_ON, service_data) + + if command == COMMAND_ACTIVATESCENE: return (SERVICE_TURN_ON, service_data) - return (SERVICE_TURN_OFF, service_data) + + if COMMAND_ONOFF == command: + if params.get('on') is True: + return (SERVICE_TURN_ON, service_data) + return (SERVICE_TURN_OFF, service_data) + + return (None, service_data) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index c21c63b0d52..dba10608991 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -181,6 +181,8 @@ def test_query_request(hass_fixture, assistant_client): 'id': "light.ceiling_lights", }, { 'id': "light.bed_light", + }, { + 'id': "light.kitchen_lights", }] } }] @@ -193,10 +195,12 @@ def test_query_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid devices = body['payload']['devices'] - assert len(devices) == 2 + assert len(devices) == 3 assert devices['light.bed_light']['on'] is False assert devices['light.ceiling_lights']['on'] is True assert devices['light.ceiling_lights']['brightness'] == 70 + assert devices['light.kitchen_lights']['color']['spectrumRGB'] == 16727919 + assert devices['light.kitchen_lights']['color']['temperature'] == 4166 @asyncio.coroutine @@ -321,6 +325,31 @@ def test_execute_request(hass_fixture, assistant_client): "on": False } }] + }, { + "devices": [{ + "id": "light.kitchen_lights", + }], + "execution": [{ + "command": "action.devices.commands.ColorAbsolute", + "params": { + "color": { + "spectrumRGB": 16711680, + "temperature": 2100 + } + } + }] + }, { + "devices": [{ + "id": "light.kitchen_lights", + }], + "execution": [{ + "command": "action.devices.commands.ColorAbsolute", + "params": { + "color": { + "spectrumRGB": 16711680 + } + } + }] }] } }] @@ -333,7 +362,10 @@ def test_execute_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid commands = body['payload']['commands'] - assert len(commands) == 3 + assert len(commands) == 5 ceiling = hass_fixture.states.get('light.ceiling_lights') assert ceiling.state == 'off' + kitchen = hass_fixture.states.get('light.kitchen_lights') + assert kitchen.attributes.get(light.ATTR_COLOR_TEMP) == 476 + assert kitchen.attributes.get(light.ATTR_RGB_COLOR) == (255, 0, 0) assert hass_fixture.states.get('switch.decorative_lights').state == 'off' diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6712b390dbb..2668c0cecfc 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -17,6 +17,57 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness const.SERVICE_TURN_ON, {'entity_id': 'light.test', 'brightness': 242} ) +}, { # Test light color temperature + 'entity_id': 'light.test', + 'command': ga.const.COMMAND_COLOR, + 'params': { + 'color': { + 'temperature': 2300, + 'name': 'warm white' + } + }, + 'expected': ( + const.SERVICE_TURN_ON, + {'entity_id': 'light.test', 'kelvin': 2300} + ) +}, { # Test light color blue + 'entity_id': 'light.test', + 'command': ga.const.COMMAND_COLOR, + 'params': { + 'color': { + 'spectrumRGB': 255, + 'name': 'blue' + } + }, + 'expected': ( + const.SERVICE_TURN_ON, + {'entity_id': 'light.test', 'rgb_color': [0, 0, 255]} + ) +}, { # Test light color yellow + 'entity_id': 'light.test', + 'command': ga.const.COMMAND_COLOR, + 'params': { + 'color': { + 'spectrumRGB': 16776960, + 'name': 'yellow' + } + }, + 'expected': ( + const.SERVICE_TURN_ON, + {'entity_id': 'light.test', 'rgb_color': [255, 255, 0]} + ) +}, { # Test unhandled action/service + 'entity_id': 'light.test', + 'command': ga.const.COMMAND_COLOR, + 'params': { + 'color': { + 'unhandled': 2300 + } + }, + 'expected': ( + None, + {'entity_id': 'light.test'} + ) }, { # Test switch to light custom type 'entity_id': 'switch.decorative_lights', 'command': ga.const.COMMAND_ONOFF,