From b04fd08cea798a9b8c5c91a1414900f62a5bb171 Mon Sep 17 00:00:00 2001 From: giefca Date: Sat, 30 Mar 2019 04:51:47 +0100 Subject: [PATCH] Google assistant: add blinds trait for covers (#22336) * Update const.py * Update smart_home.py * Update trait.py * Update test_trait.py * Update smart_home.py * Update test_trait.py * Update trait.py * Update trait.py * Update test_trait.py * Update test_trait.py * Update __init__.py * Update test_trait.py * Change email * Trying to correct CLA * Update __init__.py * Update trait.py * Update trait.py * Update trait.py * Update trait.py * Update __init__.py * Update test_trait.py * Update test_google_assistant.py * Update trait.py * Update trait.py * Update test_trait.py * Update test_trait.py --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/smart_home.py | 4 +- .../components/google_assistant/trait.py | 92 +++++++++++++----- tests/components/google_assistant/__init__.py | 20 ++-- .../components/google_assistant/test_trait.py | 94 ++++++------------- 5 files changed, 110 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 543404dd34e..852ea2469a2 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -29,6 +29,7 @@ TYPE_SCENE = PREFIX_TYPES + 'SCENE' TYPE_FAN = PREFIX_TYPES + 'FAN' TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' TYPE_LOCK = PREFIX_TYPES + 'LOCK' +TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 88cbea345b1..d84c8037c60 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -31,7 +31,7 @@ from homeassistant.components import ( from . import trait from .const import ( TYPE_LIGHT, TYPE_LOCK, TYPE_SCENE, TYPE_SWITCH, TYPE_VACUUM, - TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, + TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, TYPE_BLINDS, CONF_ALIASES, CONF_ROOM_HINT, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, @@ -45,7 +45,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { camera.DOMAIN: TYPE_CAMERA, climate.DOMAIN: TYPE_THERMOSTAT, - cover.DOMAIN: TYPE_SWITCH, + cover.DOMAIN: TYPE_BLINDS, fan.DOMAIN: TYPE_FAN, group.DOMAIN: TYPE_SWITCH, input_boolean.DOMAIN: TYPE_SWITCH, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index bd903575762..81918ff2e88 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -48,6 +48,7 @@ TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' TRAIT_MODES = PREFIX_TRAITS + 'Modes' +TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose' PREFIX_COMMANDS = 'action.devices.commands.' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' @@ -66,6 +67,7 @@ COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' COMMAND_MODES = PREFIX_COMMANDS + 'SetModes' +COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose' TRAITS = [] @@ -128,8 +130,6 @@ class BrightnessTrait(_Trait): """Test if state is supported.""" if domain == light.DOMAIN: return features & light.SUPPORT_BRIGHTNESS - if domain == cover.DOMAIN: - return features & cover.SUPPORT_SET_POSITION if domain == media_player.DOMAIN: return features & media_player.SUPPORT_VOLUME_SET @@ -149,11 +149,6 @@ class BrightnessTrait(_Trait): if brightness is not None: response['brightness'] = int(100 * (brightness / 255)) - elif domain == cover.DOMAIN: - position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) - if position is not None: - response['brightness'] = position - elif domain == media_player.DOMAIN: level = self.state.attributes.get( media_player.ATTR_MEDIA_VOLUME_LEVEL) @@ -173,12 +168,6 @@ class BrightnessTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_BRIGHTNESS_PCT: params['brightness'] }, blocking=True, context=data.context) - elif domain == cover.DOMAIN: - await self.hass.services.async_call( - cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { - ATTR_ENTITY_ID: self.state.entity_id, - cover.ATTR_POSITION: params['brightness'] - }, blocking=True, context=data.context) elif domain == media_player.DOMAIN: await self.hass.services.async_call( media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { @@ -254,7 +243,6 @@ class OnOffTrait(_Trait): switch.DOMAIN, fan.DOMAIN, light.DOMAIN, - cover.DOMAIN, media_player.DOMAIN, ) @@ -264,22 +252,13 @@ class OnOffTrait(_Trait): def query_attributes(self): """Return OnOff query attributes.""" - if self.state.domain == cover.DOMAIN: - return {'on': self.state.state != cover.STATE_CLOSED} return {'on': self.state.state != STATE_OFF} async def execute(self, command, data, params): """Execute an OnOff command.""" domain = self.state.domain - if domain == cover.DOMAIN: - service_domain = domain - if params['on']: - service = cover.SERVICE_OPEN_COVER - else: - service = cover.SERVICE_CLOSE_COVER - - elif domain == group.DOMAIN: + if domain == group.DOMAIN: service_domain = HA_DOMAIN service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF @@ -1047,3 +1026,68 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_INPUT_SOURCE: source }, blocking=True, context=data.context) + + +@register_trait +class OpenCloseTrait(_Trait): + """Trait to open and close a cover. + + https://developers.google.com/actions/smarthome/traits/openclose + """ + + name = TRAIT_OPENCLOSE + commands = [ + COMMAND_OPENCLOSE + ] + + @staticmethod + def supported(domain, features): + """Test if state is supported.""" + return domain == cover.DOMAIN + + def sync_attributes(self): + """Return opening direction.""" + return {} + + def query_attributes(self): + """Return state query attributes.""" + domain = self.state.domain + response = {} + + if domain == cover.DOMAIN: + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) + if position is not None: + response['openPercent'] = position + else: + if self.state.state != cover.STATE_CLOSED: + response['openPercent'] = 100 + else: + response['openPercent'] = 0 + + return response + + async def execute(self, command, data, params): + """Execute an Open, close, Set position command.""" + domain = self.state.domain + + if domain == cover.DOMAIN: + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) + if position is not None: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { + ATTR_ENTITY_ID: self.state.entity_id, + cover.ATTR_POSITION: params['openPercent'] + }, blocking=True, context=data.context) + else: + if self.state.state != cover.STATE_CLOSED: + if params['openPercent'] < 100: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_CLOSE_COVER, { + ATTR_ENTITY_ID: self.state.entity_id + }, blocking=True, context=data.context) + else: + if params['openPercent'] > 0: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_OPEN_COVER, { + ATTR_ENTITY_ID: self.state.entity_id + }, blocking=True, context=data.context) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index a8ea4a3f888..331c6d2d9f5 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,3 +1,5 @@ + + """Tests for the Google Assistant integration.""" DEMO_DEVICES = [{ @@ -93,9 +95,9 @@ DEMO_DEVICES = [{ 'name': 'Living Room Window' }, 'traits': - ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], + ['action.devices.traits.OpenClose'], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.BLINDS', 'willReportState': False }, { @@ -105,9 +107,9 @@ DEMO_DEVICES = [{ 'name': 'Hall Window' }, 'traits': - ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], + ['action.devices.traits.OpenClose'], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.BLINDS', 'willReportState': False }, { @@ -115,16 +117,18 @@ DEMO_DEVICES = [{ 'name': { 'name': 'Garage Door' }, - 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.SWITCH', + 'traits': ['action.devices.traits.OpenClose'], + 'type': + 'action.devices.types.BLINDS', 'willReportState': False }, { 'id': 'cover.kitchen_window', 'name': { 'name': 'Kitchen Window' }, - 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.SWITCH', + 'traits': ['action.devices.traits.OpenClose'], + 'type': + 'action.devices.types.BLINDS', 'willReportState': False }, { 'id': 'group.all_covers', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e42e4bdc915..a0a710d3d8c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -83,33 +83,6 @@ async def test_brightness_light(hass): } -async def test_brightness_cover(hass): - """Test brightness trait support for cover domain.""" - assert trait.BrightnessTrait.supported(cover.DOMAIN, - cover.SUPPORT_SET_POSITION) - - trt = trait.BrightnessTrait(hass, State('cover.bla', cover.STATE_OPEN, { - cover.ATTR_CURRENT_POSITION: 75 - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == { - 'brightness': 75 - } - - calls = async_mock_service( - hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) - await trt.execute( - trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, - {'brightness': 50}) - assert len(calls) == 1 - assert calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - cover.ATTR_POSITION: 50 - } - - async def test_brightness_media_player(hass): """Test brightness trait support for media player domain.""" assert trait.BrightnessTrait.supported(media_player.DOMAIN, @@ -358,46 +331,6 @@ async def test_onoff_light(hass): } -async def test_onoff_cover(hass): - """Test OnOff trait support for cover domain.""" - assert trait.OnOffTrait.supported(cover.DOMAIN, 0) - - trt_on = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_OPEN), - BASIC_CONFIG) - - assert trt_on.sync_attributes() == {} - - assert trt_on.query_attributes() == { - 'on': True - } - - trt_off = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_CLOSED), - BASIC_CONFIG) - - assert trt_off.query_attributes() == { - 'on': False - } - - on_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) - await trt_on.execute( - trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) - assert len(on_calls) == 1 - assert on_calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - } - - off_calls = async_mock_service(hass, cover.DOMAIN, - cover.SERVICE_CLOSE_COVER) - await trt_on.execute( - trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) - assert len(off_calls) == 1 - assert off_calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - } - - async def test_onoff_media_player(hass): """Test OnOff trait support for media_player domain.""" assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) @@ -1119,3 +1052,30 @@ async def test_modes(hass): 'entity_id': 'media_player.living_room', 'source': 'media' } + + +async def test_openclose_cover(hass): + """Test cover trait.""" + assert trait.OpenCloseTrait.supported(cover.DOMAIN, + cover.SUPPORT_SET_POSITION) + + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + cover.ATTR_CURRENT_POSITION: 75 + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == { + 'openPercent': 75 + } + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + await trt.execute( + trait.COMMAND_OPENCLOSE, BASIC_DATA, + {'openPercent': 50}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + cover.ATTR_POSITION: 50 + }