diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index f5ca9b8b146..0fd167c2729 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -22,6 +22,8 @@ from .const import ( CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, DEFAULT_ALLOW_UNLOCK ) +from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 +from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import async_register_http _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index bfeb0fcadf5..b7d3a398ef2 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -42,3 +42,8 @@ ERR_NOT_SUPPORTED = "notSupported" ERR_PROTOCOL_ERROR = 'protocolError' ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' + +# Event types +EVENT_COMMAND_RECEIVED = 'google_assistant_command_received' +EVENT_QUERY_RECEIVED = 'google_assistant_query_received' +EVENT_SYNC_RECEIVED = 'google_assistant_sync_received' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 8ea7a8aa7bc..21316c62085 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,7 +8,7 @@ from homeassistant.util.decorator import Registry from homeassistant.core import callback from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ) from homeassistant.components import ( climate, @@ -32,7 +32,8 @@ from .const import ( TYPE_THERMOSTAT, TYPE_FAN, CONF_ALIASES, CONF_ROOM_HINT, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, - ERR_UNKNOWN_ERROR + ERR_UNKNOWN_ERROR, + EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED ) from .helpers import SmartHomeError @@ -214,7 +215,8 @@ async def _process(hass, config, message): } try: - result = await handler(hass, config, inputs[0].get('payload')) + result = await handler(hass, config, request_id, + inputs[0].get('payload')) except SmartHomeError as err: return { 'requestId': request_id, @@ -233,11 +235,15 @@ async def _process(hass, config, message): @HANDLERS.register('action.devices.SYNC') -async def async_devices_sync(hass, config, payload): +async def async_devices_sync(hass, config, request_id, payload): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ + hass.bus.async_fire(EVENT_SYNC_RECEIVED, { + 'request_id': request_id + }) + devices = [] for state in hass.states.async_all(): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: @@ -255,14 +261,16 @@ async def async_devices_sync(hass, config, payload): devices.append(serialized) - return { + response = { 'agentUserId': config.agent_user_id, 'devices': devices, } + return response + @HANDLERS.register('action.devices.QUERY') -async def async_devices_query(hass, config, payload): +async def async_devices_query(hass, config, request_id, payload): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery @@ -272,6 +280,11 @@ async def async_devices_query(hass, config, payload): devid = device['id'] state = hass.states.get(devid) + hass.bus.async_fire(EVENT_QUERY_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: devid, + }) + if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} @@ -283,7 +296,7 @@ async def async_devices_query(hass, config, payload): @HANDLERS.register('action.devices.EXECUTE') -async def handle_devices_execute(hass, config, payload): +async def handle_devices_execute(hass, config, request_id, payload): """Handle action.devices.EXECUTE request. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute @@ -296,6 +309,12 @@ async def handle_devices_execute(hass, config, payload): command['execution']): entity_id = device['id'] + hass.bus.async_fire(EVENT_COMMAND_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: entity_id, + 'execution': execution + }) + # Happens if error occurred. Skip entity for further processing if entity_id in results: continue @@ -341,7 +360,7 @@ async def handle_devices_execute(hass, config, payload): @HANDLERS.register('action.devices.DISCONNECT') -async def async_devices_disconnect(hass, config, payload): +async def async_devices_disconnect(hass, config, request_id, payload): """Handle action.devices.DISCONNECT request. https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 05ae0809527..d1ec80844b6 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -7,7 +7,8 @@ from homeassistant.components.climate.const import ( ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE ) from homeassistant.components.google_assistant import ( - const, trait, helpers, smart_home as sh) + const, trait, helpers, smart_home as sh, + EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) from homeassistant.components.light.demo import DemoLight @@ -48,6 +49,9 @@ async def test_sync_message(hass): } ) + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message(hass, config, { "requestId": REQ_ID, "inputs": [{ @@ -85,6 +89,13 @@ async def test_sync_message(hass): }] } } + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == EVENT_SYNC_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + } async def test_query_message(hass): @@ -109,6 +120,9 @@ async def test_query_message(hass): light2.entity_id = 'light.another_light' await light2.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append) + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -149,12 +163,33 @@ async def test_query_message(hass): } } + assert len(events) == 3 + assert events[0].event_type == EVENT_QUERY_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.demo_light' + } + assert events[1].event_type == EVENT_QUERY_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.another_light' + } + assert events[2].event_type == EVENT_QUERY_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing' + } + async def test_execute(hass): """Test an execute command.""" await async_setup_component(hass, 'light', { 'light': {'platform': 'demo'} }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.ceiling_lights'}, blocking=True) @@ -209,6 +244,52 @@ async def test_execute(hass): } } + assert len(events) == 4 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[1].event_type == EVENT_COMMAND_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + assert events[2].event_type == EVENT_COMMAND_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[3].event_type == EVENT_COMMAND_RECEIVED + assert events[3].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" @@ -218,6 +299,11 @@ async def test_raising_error_trait(hass): ATTR_SUPPORTED_FEATURES: SUPPORT_OPERATION_MODE, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.async_block_till_done() + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -250,6 +336,19 @@ async def test_raising_error_trait(hass): } } + assert len(events) == 1 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'climate.bla', + 'execution': { + 'command': 'action.devices.commands.ThermostatTemperatureSetpoint', + 'params': { + 'thermostatTemperatureSetpoint': 10 + } + } + } + def test_serialize_input_boolean(): """Test serializing an input boolean entity."""