From ddeeba20b95f033b99185f12a1c09270624430d3 Mon Sep 17 00:00:00 2001 From: Eric Nagley Date: Sun, 11 Nov 2018 16:02:33 -0500 Subject: [PATCH] Google assistant enable fan speed controls (#18373) * Initial commit of Traits changes. * Initial commit of tests chagnes for added FanSpeed trait. * pylint fixes. * Default reversible to false * Ensure reversible returns True/False. * Fix FanSpeed trait name and fix order. * Add remaining checks to FanSpeed trait Test. * Remove un-needed blank lines at EOF. * Update homeassistant/components/google_assistant/trait.py Co-Authored-By: marchingphoenix * use fan.SPEED_* constants as keys to speed_synonyms dict. convert True if() to bool() for reversible assignment. * use fan.SPEED_OFF constant of 'on' check. --- .../components/google_assistant/trait.py | 79 +++++++++++++++- tests/components/google_assistant/__init__.py | 10 ++- .../components/google_assistant/test_trait.py | 90 +++++++++++++++++-- 3 files changed, 170 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index ce13818d9de..d32dd91a3c1 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,7 +1,6 @@ """Implement the Smart Home traits.""" import logging -from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components import ( climate, cover, @@ -26,8 +25,8 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ) +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util - from .const import ERR_VALUE_OUT_OF_RANGE from .helpers import SmartHomeError @@ -43,6 +42,7 @@ TRAIT_COLOR_TEMP = PREFIX_TRAITS + 'ColorTemperature' TRAIT_SCENE = PREFIX_TRAITS + 'Scene' TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' +TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' PREFIX_COMMANDS = 'action.devices.commands.' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' @@ -58,6 +58,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = ( PREFIX_COMMANDS + 'ThermostatTemperatureSetRange') COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' +COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' TRAITS = [] @@ -675,3 +676,77 @@ class LockUnlockTrait(_Trait): await self.hass.services.async_call(lock.DOMAIN, service, { ATTR_ENTITY_ID: self.state.entity_id }, blocking=True) + + +@register_trait +class FanSpeedTrait(_Trait): + """Trait to control speed of Fan. + + https://developers.google.com/actions/smarthome/traits/fanspeed + """ + + name = TRAIT_FANSPEED + commands = [ + COMMAND_FANSPEED + ] + + speed_synonyms = { + fan.SPEED_OFF: ['stop', 'off'], + fan.SPEED_LOW: ['slow', 'low', 'slowest', 'lowest'], + fan.SPEED_MEDIUM: ['medium', 'mid', 'middle'], + fan.SPEED_HIGH: [ + 'high', 'max', 'fast', 'highest', 'fastest', 'maximum' + ] + } + + @staticmethod + def supported(domain, features): + """Test if state is supported.""" + if domain != fan.DOMAIN: + return False + + return features & fan.SUPPORT_SET_SPEED + + def sync_attributes(self): + """Return speed point and modes attributes for a sync request.""" + modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) + speeds = [] + for mode in modes: + speed = { + "speed_name": mode, + "speed_values": [{ + "speed_synonym": self.speed_synonyms.get(mode), + "lang": 'en' + }] + } + speeds.append(speed) + + return { + 'availableFanSpeeds': { + 'speeds': speeds, + 'ordered': True + }, + "reversible": bool(self.state.attributes.get( + ATTR_SUPPORTED_FEATURES, 0) & fan.SUPPORT_DIRECTION) + } + + def query_attributes(self): + """Return speed point and modes query attributes.""" + attrs = self.state.attributes + response = {} + + speed = attrs.get(fan.ATTR_SPEED) + if speed is not None: + response['on'] = speed != fan.SPEED_OFF + response['online'] = True + response['currentFanSpeedSetting'] = speed + + return response + + async def execute(self, command, params): + """Execute an SetFanSpeed command.""" + await self.hass.services.async_call( + fan.DOMAIN, fan.SERVICE_SET_SPEED, { + ATTR_ENTITY_ID: self.state.entity_id, + fan.ATTR_SPEED: params['fanSpeed'] + }, blocking=True) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 5fd00abc411..1568919a9b4 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -183,7 +183,10 @@ DEMO_DEVICES = [{ 'name': { 'name': 'Living Room Fan' }, - 'traits': ['action.devices.traits.OnOff'], + 'traits': [ + 'action.devices.traits.FanSpeed', + 'action.devices.traits.OnOff' + ], 'type': 'action.devices.types.FAN', 'willReportState': False }, { @@ -191,7 +194,10 @@ DEMO_DEVICES = [{ 'name': { 'name': 'Ceiling Fan' }, - 'traits': ['action.devices.traits.OnOff'], + 'traits': [ + 'action.devices.traits.FanSpeed', + 'action.devices.traits.OnOff' + ], 'type': 'action.devices.types.FAN', 'willReportState': False }, { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index ff3ce65ee27..42af1230eed 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,10 +1,6 @@ """Tests for the Google Assistant traits.""" import pytest -from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES) -from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.components import ( climate, cover, @@ -20,8 +16,11 @@ from homeassistant.components import ( group, ) from homeassistant.components.google_assistant import trait, helpers, const +from homeassistant.const import ( + STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES) +from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.util import color - from tests.common import async_mock_service BASIC_CONFIG = helpers.Config( @@ -795,3 +794,84 @@ async def test_lock_unlock_unlock(hass): assert calls[0].data == { ATTR_ENTITY_ID: 'lock.front_door' } + + +async def test_fan_speed(hass): + """Test FanSpeed trait speed control support for fan domain.""" + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED) + + trt = trait.FanSpeedTrait( + hass, State( + 'fan.living_room_fan', fan.SPEED_HIGH, attributes={ + 'speed_list': [ + fan.SPEED_OFF, fan.SPEED_LOW, fan.SPEED_MEDIUM, + fan.SPEED_HIGH + ], + 'speed': 'low' + }), BASIC_CONFIG) + + assert trt.sync_attributes() == { + 'availableFanSpeeds': { + 'ordered': True, + 'speeds': [ + { + 'speed_name': 'off', + 'speed_values': [ + { + 'speed_synonym': ['stop', 'off'], + 'lang': 'en' + } + ] + }, + { + 'speed_name': 'low', + 'speed_values': [ + { + 'speed_synonym': [ + 'slow', 'low', 'slowest', 'lowest'], + 'lang': 'en' + } + ] + }, + { + 'speed_name': 'medium', + 'speed_values': [ + { + 'speed_synonym': ['medium', 'mid', 'middle'], + 'lang': 'en' + } + ] + }, + { + 'speed_name': 'high', + 'speed_values': [ + { + 'speed_synonym': [ + 'high', 'max', 'fast', 'highest', 'fastest', + 'maximum'], + 'lang': 'en' + } + ] + } + ] + }, + 'reversible': False + } + + assert trt.query_attributes() == { + 'currentFanSpeedSetting': 'low', + 'on': True, + 'online': True + } + + assert trt.can_execute( + trait.COMMAND_FANSPEED, params={'fanSpeed': 'medium'}) + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED) + await trt.execute(trait.COMMAND_FANSPEED, params={'fanSpeed': 'medium'}) + + assert len(calls) == 1 + assert calls[0].data == { + 'entity_id': 'fan.living_room_fan', + 'speed': 'medium' + }