From 4da8ec0a05615e51559f2a5e2213f182d0077006 Mon Sep 17 00:00:00 2001 From: Eitan Mosenkis Date: Wed, 1 Nov 2017 16:44:59 +0200 Subject: [PATCH] Add Google Assistant support for setting climate temperature and operation mode. (#10174) Fixes #10025. --- .../components/google_assistant/const.py | 10 ++++- .../components/google_assistant/smart_home.py | 38 ++++++++++++++++--- tests/components/google_assistant/__init__.py | 28 ++++++++++++++ .../google_assistant/test_google_assistant.py | 32 ++++++++++------ .../google_assistant/test_smart_home.py | 29 ++++++++++++++ 5 files changed, 120 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index eb82f5d3e70..80afad82938 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -16,8 +16,9 @@ CONF_ALIASES = 'aliases' DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ - 'switch', 'light', 'group', 'media_player', 'fan', 'cover' + 'switch', 'light', 'group', 'media_player', 'fan', 'cover', 'climate' ] +CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', 'heatcool'} PREFIX_TRAITS = 'action.devices.traits.' TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' @@ -25,14 +26,21 @@ TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness' TRAIT_RGB_COLOR = PREFIX_TRAITS + 'ColorSpectrum' TRAIT_COLOR_TEMP = PREFIX_TRAITS + 'ColorTemperature' TRAIT_SCENE = PREFIX_TRAITS + 'Scene' +TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' PREFIX_COMMANDS = 'action.devices.commands.' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' COMMAND_BRIGHTNESS = PREFIX_COMMANDS + 'BrightnessAbsolute' COMMAND_COLOR = PREFIX_COMMANDS + 'ColorAbsolute' COMMAND_ACTIVATESCENE = PREFIX_COMMANDS + 'ActivateScene' +COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = ( + PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint') +COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = ( + PREFIX_COMMANDS + 'ThermostatTemperatureSetRange') +COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' PREFIX_TYPES = 'action.devices.types.' TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' TYPE_SCENE = PREFIX_TYPES + 'SCENE' +TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 57da74b590a..1c8adf3d8f7 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -15,16 +15,18 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON ) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script + switch, light, cover, media_player, group, fan, scene, script, climate ) from .const import ( ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE, COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE, + COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, + COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE, TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP, - TRAIT_RGB_COLOR, TRAIT_SCENE, - TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, - CONF_ALIASES, + TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING, + TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT, + CONF_ALIASES, CLIMATE_SUPPORTED_MODES ) _LOGGER = logging.getLogger(__name__) @@ -54,11 +56,12 @@ MAPPING_COMPONENT = { media_player.SUPPORT_VOLUME_SET: TRAIT_BRIGHTNESS } ], + climate.DOMAIN: [TYPE_THERMOSTAT, TRAIT_TEMPERATURE_SETTING, None], } # type: Dict[str, list] def make_actions_response(request_id: str, payload: dict) -> dict: - """Helper to simplify format for response.""" + """Make response message.""" return {'requestId': request_id, 'payload': payload} @@ -96,6 +99,14 @@ def entity_to_device(entity: Entity): for feature, trait in class_data[2].items(): if feature & supported > 0: device['traits'].append(trait) + if entity.domain == climate.DOMAIN: + modes = ','.join( + m for m in entity.attributes.get(climate.ATTR_OPERATION_LIST, []) + if m in CLIMATE_SUPPORTED_MODES) + device['attributes'] = { + 'availableThermostatModes': modes, + 'thermostatTemperatureUnit': 'C', + } return device @@ -152,6 +163,23 @@ def determine_service(entity_id: str, command: str, return (cover.SERVICE_OPEN_COVER, service_data) return (cover.SERVICE_CLOSE_COVER, service_data) + # special climate handling + if domain == climate.DOMAIN: + if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: + service_data['temperature'] = params.get( + 'thermostatTemperatureSetpoint', 25) + return (climate.SERVICE_SET_TEMPERATURE, service_data) + if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: + service_data['target_temp_high'] = params.get( + 'thermostatTemperatureSetpointHigh', 25) + service_data['target_temp_low'] = params.get( + 'thermostatTemperatureSetpointLow', 18) + return (climate.SERVICE_SET_TEMPERATURE, service_data) + if command == COMMAND_THERMOSTAT_SET_MODE: + service_data['operation_mode'] = params.get( + 'thermostatMode', 'off') + return (climate.SERVICE_SET_OPERATION_MODE, service_data) + if command == COMMAND_BRIGHTNESS: brightness = params.get('brightness') service_data['brightness'] = int(brightness / 100 * 255) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 7a80915bcec..f424fb92647 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -202,4 +202,32 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.Scene'], 'type': 'action.devices.types.SCENE', 'willReportState': False +}, { + 'id': 'climate.hvac', + 'name': { + 'name': 'Hvac' + }, + 'traits': ['action.devices.traits.TemperatureSetting'], + 'type': 'action.devices.types.THERMOSTAT', + 'willReportState': False, + 'attributes': { + 'availableThermostatModes': 'heat,cool,off', + 'thermostatTemperatureUnit': 'C', + }, +}, { + 'id': 'climate.heatpump', + 'name': { + 'name': 'HeatPump' + }, + 'traits': ['action.devices.traits.TemperatureSetting'], + 'type': 'action.devices.types.THERMOSTAT', + 'willReportState': False +}, { + 'id': 'climate.ecobee', + 'name': { + 'name': 'Ecobee' + }, + 'traits': ['action.devices.traits.TemperatureSetting'], + 'type': 'action.devices.types.THERMOSTAT', + 'willReportState': False }] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 46a8e410cca..35be79469a9 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -6,7 +6,7 @@ import pytest from homeassistant import setup, const, core from homeassistant.components import ( - http, async_setup, light, cover, media_player, fan, switch + http, async_setup, light, cover, media_player, fan, switch, climate ) from homeassistant.components import google_assistant as ga from tests.common import get_test_instance_port @@ -45,7 +45,7 @@ def assistant_client(loop, hass_fixture, test_client): @pytest.fixture def hass_fixture(loop, hass): - """Setup a hass instance for these tests.""" + """Set up a hass instance for these tests.""" # We need to do this to get access to homeassistant/turn_(on,off) loop.run_until_complete(async_setup(hass, {core.DOMAIN: {}})) @@ -89,6 +89,13 @@ def hass_fixture(loop, hass): }] })) + loop.run_until_complete( + setup.async_setup_component(hass, climate.DOMAIN, { + 'climate': [{ + 'platform': 'demo' + }] + })) + # Kitchen light is explicitly excluded from being exposed ceiling_lights_entity = hass.states.get('light.ceiling_lights') attrs = dict(ceiling_lights_entity.attributes) @@ -142,15 +149,18 @@ def test_sync_request(hass_fixture, assistant_client): body = yield from result.json() assert body.get('requestId') == reqid devices = body['payload']['devices'] - # assert len(devices) == 4 - assert len(devices) == len(DEMO_DEVICES) - # HACK this is kind of slow and lazy - for dev in devices: - for demo in DEMO_DEVICES: - if dev['id'] == demo['id']: - assert dev['name'] == demo['name'] - assert set(dev['traits']) == set(demo['traits']) - assert dev['type'] == demo['type'] + assert ( + sorted([dev['id'] for dev in devices]) + == sorted([dev['id'] for dev in DEMO_DEVICES])) + + for dev, demo in zip( + sorted(devices, key=lambda d: d['id']), + sorted(DEMO_DEVICES, key=lambda d: d['id'])): + assert dev['name'] == demo['name'] + assert set(dev['traits']) == set(demo['traits']) + assert dev['type'] == demo['type'] + if 'attributes' in demo: + assert dev['attributes'] == demo['attributes'] @asyncio.coroutine diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 7736197dd2c..20db85b998e 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,6 +3,7 @@ import asyncio from homeassistant import const +from homeassistant.components import climate from homeassistant.components import google_assistant as ga DETERMINE_SERVICE_TESTS = [{ # Test light brightness @@ -73,6 +74,34 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness const.SERVICE_VOLUME_SET, {'entity_id': 'media_player.living_room', 'volume_level': 0.3} ), +}, { # Test climate temperature + 'entity_id': 'climate.living_room', + 'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, + 'params': {'thermostatTemperatureSetpoint': 24.5}, + 'expected': ( + climate.SERVICE_SET_TEMPERATURE, + {'entity_id': 'climate.living_room', 'temperature': 24.5} + ), +}, { # Test climate temperature range + 'entity_id': 'climate.living_room', + 'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, + 'params': { + 'thermostatTemperatureSetpointHigh': 24.5, + 'thermostatTemperatureSetpointLow': 20.5, + }, + 'expected': ( + climate.SERVICE_SET_TEMPERATURE, + {'entity_id': 'climate.living_room', + 'target_temp_high': 24.5, 'target_temp_low': 20.5} + ), +}, { # Test climate operation mode + 'entity_id': 'climate.living_room', + 'command': ga.const.COMMAND_THERMOSTAT_SET_MODE, + 'params': {'thermostatMode': 'heat'}, + 'expected': ( + climate.SERVICE_SET_OPERATION_MODE, + {'entity_id': 'climate.living_room', 'operation_mode': 'heat'} + ), }]