Google Assistant: Create and pass context to service calls (#21551)

* Google Assistant: Create and pass context to service calls

* Refactor request data into separate object and pass to execute.
This commit is contained in:
Penny Wood 2019-03-06 12:00:53 +08:00 committed by Paulus Schoutsen
parent fc1ee9be43
commit d1038ea79f
8 changed files with 345 additions and 265 deletions

View File

@ -236,7 +236,6 @@ class Cloud:
self._gactions_config = ga_h.Config( self._gactions_config = ga_h.Config(
should_expose=should_expose, should_expose=should_expose,
allow_unlock=self.prefs.google_allow_unlock, allow_unlock=self.prefs.google_allow_unlock,
agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG), entity_config=conf.get(CONF_ENTITY_CONFIG),
) )

View File

@ -334,7 +334,9 @@ def async_handle_google_actions(hass, cloud, payload):
return ga.turned_off_response(payload) return ga.turned_off_response(payload)
result = yield from ga.async_handle_message( result = yield from ga.async_handle_message(
hass, cloud.gactions_config, payload) hass, cloud.gactions_config,
cloud.claims['cognito:username'],
payload)
return result return result

View File

@ -1,4 +1,5 @@
"""Helper classes for Google Assistant integration.""" """Helper classes for Google Assistant integration."""
from homeassistant.core import Context
class SmartHomeError(Exception): class SmartHomeError(Exception):
@ -16,10 +17,19 @@ class SmartHomeError(Exception):
class Config: class Config:
"""Hold the configuration for Google Assistant.""" """Hold the configuration for Google Assistant."""
def __init__(self, should_expose, allow_unlock, agent_user_id, def __init__(self, should_expose, allow_unlock,
entity_config=None): entity_config=None):
"""Initialize the configuration.""" """Initialize the configuration."""
self.should_expose = should_expose self.should_expose = should_expose
self.agent_user_id = agent_user_id
self.entity_config = entity_config or {} self.entity_config = entity_config or {}
self.allow_unlock = allow_unlock self.allow_unlock = allow_unlock
class RequestData:
"""Hold data associated with a particular request."""
def __init__(self, config, user_id, request_id):
"""Initialize the request data."""
self.config = config
self.request_id = request_id
self.context = Context(user_id=user_id)

View File

@ -71,17 +71,16 @@ class GoogleAssistantView(HomeAssistantView):
def __init__(self, is_exposed, entity_config, allow_unlock): def __init__(self, is_exposed, entity_config, allow_unlock):
"""Initialize the Google Assistant request handler.""" """Initialize the Google Assistant request handler."""
self.is_exposed = is_exposed self.config = Config(is_exposed,
self.entity_config = entity_config allow_unlock,
self.allow_unlock = allow_unlock entity_config)
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
"""Handle Google Assistant requests.""" """Handle Google Assistant requests."""
message = await request.json() # type: dict message = await request.json() # type: dict
config = Config(self.is_exposed,
self.allow_unlock,
request['hass_user'].id,
self.entity_config)
result = await async_handle_message( result = await async_handle_message(
request.app['hass'], config, message) request.app['hass'],
self.config,
request['hass_user'].id,
message)
return self.json(result) return self.json(result)

View File

@ -36,7 +36,7 @@ from .const import (
ERR_UNKNOWN_ERROR, ERR_UNKNOWN_ERROR,
EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
) )
from .helpers import SmartHomeError from .helpers import SmartHomeError, RequestData
HANDLERS = Registry() HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -87,7 +87,8 @@ class _GoogleEntity:
domain = state.domain domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
return [Trait(self.hass, state, self.config) for Trait in trait.TRAITS return [Trait(self.hass, state, self.config)
for Trait in trait.TRAITS
if Trait.supported(domain, features)] if Trait.supported(domain, features)]
async def sync_serialize(self): async def sync_serialize(self):
@ -178,7 +179,7 @@ class _GoogleEntity:
return attrs return attrs
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a command. """Execute a command.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
@ -186,7 +187,7 @@ class _GoogleEntity:
executed = False executed = False
for trt in self.traits(): for trt in self.traits():
if trt.can_execute(command, params): if trt.can_execute(command, params):
await trt.execute(command, params) await trt.execute(command, data, params)
executed = True executed = True
break break
@ -202,9 +203,13 @@ class _GoogleEntity:
self.state = self.hass.states.get(self.entity_id) self.state = self.hass.states.get(self.entity_id)
async def async_handle_message(hass, config, message): async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages.""" """Handle incoming API messages."""
response = await _process(hass, config, message) request_id = message.get('requestId') # type: str
data = RequestData(config, user_id, request_id)
response = await _process(hass, data, message)
if response and 'errorCode' in response['payload']: if response and 'errorCode' in response['payload']:
_LOGGER.error('Error handling message %s: %s', _LOGGER.error('Error handling message %s: %s',
@ -213,14 +218,13 @@ async def async_handle_message(hass, config, message):
return response return response
async def _process(hass, config, message): async def _process(hass, data, message):
"""Process a message.""" """Process a message."""
request_id = message.get('requestId') # type: str
inputs = message.get('inputs') # type: list inputs = message.get('inputs') # type: list
if len(inputs) != 1: if len(inputs) != 1:
return { return {
'requestId': request_id, 'requestId': data.request_id,
'payload': {'errorCode': ERR_PROTOCOL_ERROR} 'payload': {'errorCode': ERR_PROTOCOL_ERROR}
} }
@ -228,49 +232,49 @@ async def _process(hass, config, message):
if handler is None: if handler is None:
return { return {
'requestId': request_id, 'requestId': data.request_id,
'payload': {'errorCode': ERR_PROTOCOL_ERROR} 'payload': {'errorCode': ERR_PROTOCOL_ERROR}
} }
try: try:
result = await handler(hass, config, request_id, result = await handler(hass, data, inputs[0].get('payload'))
inputs[0].get('payload'))
except SmartHomeError as err: except SmartHomeError as err:
return { return {
'requestId': request_id, 'requestId': data.request_id,
'payload': {'errorCode': err.code} 'payload': {'errorCode': err.code}
} }
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Unexpected error') _LOGGER.exception('Unexpected error')
return { return {
'requestId': request_id, 'requestId': data.request_id,
'payload': {'errorCode': ERR_UNKNOWN_ERROR} 'payload': {'errorCode': ERR_UNKNOWN_ERROR}
} }
if result is None: if result is None:
return None return None
return {'requestId': request_id, 'payload': result} return {'requestId': data.request_id, 'payload': result}
@HANDLERS.register('action.devices.SYNC') @HANDLERS.register('action.devices.SYNC')
async def async_devices_sync(hass, config, request_id, payload): async def async_devices_sync(hass, data, payload):
"""Handle action.devices.SYNC request. """Handle action.devices.SYNC request.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync https://developers.google.com/actions/smarthome/create-app#actiondevicessync
""" """
hass.bus.async_fire(EVENT_SYNC_RECEIVED, { hass.bus.async_fire(
'request_id': request_id EVENT_SYNC_RECEIVED,
}) {'request_id': data.request_id},
context=data.context)
devices = [] devices = []
for state in hass.states.async_all(): for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue continue
if not config.should_expose(state): if not data.config.should_expose(state):
continue continue
entity = _GoogleEntity(hass, config, state) entity = _GoogleEntity(hass, data.config, state)
serialized = await entity.sync_serialize() serialized = await entity.sync_serialize()
if serialized is None: if serialized is None:
@ -280,7 +284,7 @@ async def async_devices_sync(hass, config, request_id, payload):
devices.append(serialized) devices.append(serialized)
response = { response = {
'agentUserId': config.agent_user_id, 'agentUserId': data.context.user_id,
'devices': devices, 'devices': devices,
} }
@ -288,7 +292,7 @@ async def async_devices_sync(hass, config, request_id, payload):
@HANDLERS.register('action.devices.QUERY') @HANDLERS.register('action.devices.QUERY')
async def async_devices_query(hass, config, request_id, payload): async def async_devices_query(hass, data, payload):
"""Handle action.devices.QUERY request. """Handle action.devices.QUERY request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
@ -298,23 +302,27 @@ async def async_devices_query(hass, config, request_id, payload):
devid = device['id'] devid = device['id']
state = hass.states.get(devid) state = hass.states.get(devid)
hass.bus.async_fire(EVENT_QUERY_RECEIVED, { hass.bus.async_fire(
'request_id': request_id, EVENT_QUERY_RECEIVED,
ATTR_ENTITY_ID: devid, {
}) 'request_id': data.request_id,
ATTR_ENTITY_ID: devid,
},
context=data.context)
if not state: if not state:
# If we can't find a state, the device is offline # If we can't find a state, the device is offline
devices[devid] = {'online': False} devices[devid] = {'online': False}
continue continue
devices[devid] = _GoogleEntity(hass, config, state).query_serialize() entity = _GoogleEntity(hass, data.config, state)
devices[devid] = entity.query_serialize()
return {'devices': devices} return {'devices': devices}
@HANDLERS.register('action.devices.EXECUTE') @HANDLERS.register('action.devices.EXECUTE')
async def handle_devices_execute(hass, config, request_id, payload): async def handle_devices_execute(hass, data, payload):
"""Handle action.devices.EXECUTE request. """Handle action.devices.EXECUTE request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
@ -327,11 +335,14 @@ async def handle_devices_execute(hass, config, request_id, payload):
command['execution']): command['execution']):
entity_id = device['id'] entity_id = device['id']
hass.bus.async_fire(EVENT_COMMAND_RECEIVED, { hass.bus.async_fire(
'request_id': request_id, EVENT_COMMAND_RECEIVED,
ATTR_ENTITY_ID: entity_id, {
'execution': execution 'request_id': data.request_id,
}) ATTR_ENTITY_ID: entity_id,
'execution': execution
},
context=data.context)
# Happens if error occurred. Skip entity for further processing # Happens if error occurred. Skip entity for further processing
if entity_id in results: if entity_id in results:
@ -348,10 +359,11 @@ async def handle_devices_execute(hass, config, request_id, payload):
} }
continue continue
entities[entity_id] = _GoogleEntity(hass, config, state) entities[entity_id] = _GoogleEntity(hass, data.config, state)
try: try:
await entities[entity_id].execute(execution['command'], await entities[entity_id].execute(execution['command'],
data,
execution.get('params', {})) execution.get('params', {}))
except SmartHomeError as err: except SmartHomeError as err:
results[entity_id] = { results[entity_id] = {
@ -378,7 +390,7 @@ async def handle_devices_execute(hass, config, request_id, payload):
@HANDLERS.register('action.devices.DISCONNECT') @HANDLERS.register('action.devices.DISCONNECT')
async def async_devices_disconnect(hass, config, request_id, payload): async def async_devices_disconnect(hass, data, payload):
"""Handle action.devices.DISCONNECT request. """Handle action.devices.DISCONNECT request.
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect

View File

@ -102,7 +102,7 @@ class _Trait:
"""Test if command can be executed.""" """Test if command can be executed."""
return command in self.commands return command in self.commands
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a trait command.""" """Execute a trait command."""
raise NotImplementedError raise NotImplementedError
@ -159,7 +159,7 @@ class BrightnessTrait(_Trait):
return response return response
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a brightness command.""" """Execute a brightness command."""
domain = self.state.domain domain = self.state.domain
@ -168,20 +168,20 @@ class BrightnessTrait(_Trait):
light.DOMAIN, light.SERVICE_TURN_ON, { light.DOMAIN, light.SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_BRIGHTNESS_PCT: params['brightness'] light.ATTR_BRIGHTNESS_PCT: params['brightness']
}, blocking=True) }, blocking=True, context=data.context)
elif domain == cover.DOMAIN: elif domain == cover.DOMAIN:
await self.hass.services.async_call( await self.hass.services.async_call(
cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
cover.ATTR_POSITION: params['brightness'] cover.ATTR_POSITION: params['brightness']
}, blocking=True) }, blocking=True, context=data.context)
elif domain == media_player.DOMAIN: elif domain == media_player.DOMAIN:
await self.hass.services.async_call( await self.hass.services.async_call(
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: media_player.ATTR_MEDIA_VOLUME_LEVEL:
params['brightness'] / 100 params['brightness'] / 100
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -221,7 +221,7 @@ class OnOffTrait(_Trait):
return {'on': self.state.state != cover.STATE_CLOSED} return {'on': self.state.state != cover.STATE_CLOSED}
return {'on': self.state.state != STATE_OFF} return {'on': self.state.state != STATE_OFF}
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute an OnOff command.""" """Execute an OnOff command."""
domain = self.state.domain domain = self.state.domain
@ -242,7 +242,7 @@ class OnOffTrait(_Trait):
await self.hass.services.async_call(service_domain, service, { await self.hass.services.async_call(service_domain, service, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -288,7 +288,7 @@ class ColorSpectrumTrait(_Trait):
return (command in self.commands and return (command in self.commands and
'spectrumRGB' in params.get('color', {})) 'spectrumRGB' in params.get('color', {}))
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a color spectrum command.""" """Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6 # Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB']) hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
@ -298,7 +298,7 @@ class ColorSpectrumTrait(_Trait):
await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_HS_COLOR: color light.ATTR_HS_COLOR: color
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -355,7 +355,7 @@ class ColorTemperatureTrait(_Trait):
return (command in self.commands and return (command in self.commands and
'temperature' in params.get('color', {})) 'temperature' in params.get('color', {}))
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a color temperature command.""" """Execute a color temperature command."""
temp = color_util.color_temperature_kelvin_to_mired( temp = color_util.color_temperature_kelvin_to_mired(
params['color']['temperature']) params['color']['temperature'])
@ -371,7 +371,7 @@ class ColorTemperatureTrait(_Trait):
await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_COLOR_TEMP: temp, light.ATTR_COLOR_TEMP: temp,
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -400,13 +400,14 @@ class SceneTrait(_Trait):
"""Return scene query attributes.""" """Return scene query attributes."""
return {} return {}
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a scene command.""" """Execute a scene command."""
# Don't block for scripts as they can be slow. # Don't block for scripts as they can be slow.
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, SERVICE_TURN_ON, { self.state.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=self.state.domain != script.DOMAIN) }, blocking=self.state.domain != script.DOMAIN,
context=data.context)
@register_trait @register_trait
@ -434,12 +435,12 @@ class DockTrait(_Trait):
"""Return dock query attributes.""" """Return dock query attributes."""
return {'isDocked': self.state.state == vacuum.STATE_DOCKED} return {'isDocked': self.state.state == vacuum.STATE_DOCKED}
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a dock command.""" """Execute a dock command."""
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, { self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -473,30 +474,30 @@ class StartStopTrait(_Trait):
'isPaused': self.state.state == vacuum.STATE_PAUSED, 'isPaused': self.state.state == vacuum.STATE_PAUSED,
} }
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a StartStop command.""" """Execute a StartStop command."""
if command == COMMAND_STARTSTOP: if command == COMMAND_STARTSTOP:
if params['start']: if params['start']:
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, vacuum.SERVICE_START, { self.state.domain, vacuum.SERVICE_START, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
else: else:
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, vacuum.SERVICE_STOP, { self.state.domain, vacuum.SERVICE_STOP, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
elif command == COMMAND_PAUSEUNPAUSE: elif command == COMMAND_PAUSEUNPAUSE:
if params['pause']: if params['pause']:
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, vacuum.SERVICE_PAUSE, { self.state.domain, vacuum.SERVICE_PAUSE, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
else: else:
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, vacuum.SERVICE_START, { self.state.domain, vacuum.SERVICE_START, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -584,7 +585,7 @@ class TemperatureSettingTrait(_Trait):
return response return response
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute a temperature point or mode command.""" """Execute a temperature point or mode command."""
# All sent in temperatures are always in Celsius # All sent in temperatures are always in Celsius
unit = self.hass.config.units.temperature_unit unit = self.hass.config.units.temperature_unit
@ -608,7 +609,7 @@ class TemperatureSettingTrait(_Trait):
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, { climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
ATTR_TEMPERATURE: temp ATTR_TEMPERATURE: temp
}, blocking=True) }, blocking=True, context=data.context)
elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
temp_high = temp_util.convert( temp_high = temp_util.convert(
@ -640,7 +641,7 @@ class TemperatureSettingTrait(_Trait):
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_TARGET_TEMP_HIGH: temp_high, climate.ATTR_TARGET_TEMP_HIGH: temp_high,
climate.ATTR_TARGET_TEMP_LOW: temp_low, climate.ATTR_TARGET_TEMP_LOW: temp_low,
}, blocking=True) }, blocking=True, context=data.context)
elif command == COMMAND_THERMOSTAT_SET_MODE: elif command == COMMAND_THERMOSTAT_SET_MODE:
await self.hass.services.async_call( await self.hass.services.async_call(
@ -648,7 +649,7 @@ class TemperatureSettingTrait(_Trait):
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_OPERATION_MODE: climate.ATTR_OPERATION_MODE:
self.google_to_hass[params['thermostatMode']], self.google_to_hass[params['thermostatMode']],
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -681,7 +682,7 @@ class LockUnlockTrait(_Trait):
allowed_unlock = not params['lock'] and self.config.allow_unlock allowed_unlock = not params['lock'] and self.config.allow_unlock
return params['lock'] or allowed_unlock return params['lock'] or allowed_unlock
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute an LockUnlock command.""" """Execute an LockUnlock command."""
if params['lock']: if params['lock']:
service = lock.SERVICE_LOCK service = lock.SERVICE_LOCK
@ -690,7 +691,7 @@ class LockUnlockTrait(_Trait):
await self.hass.services.async_call(lock.DOMAIN, service, { await self.hass.services.async_call(lock.DOMAIN, service, {
ATTR_ENTITY_ID: self.state.entity_id ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -760,13 +761,13 @@ class FanSpeedTrait(_Trait):
return response return response
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute an SetFanSpeed command.""" """Execute an SetFanSpeed command."""
await self.hass.services.async_call( await self.hass.services.async_call(
fan.DOMAIN, fan.SERVICE_SET_SPEED, { fan.DOMAIN, fan.SERVICE_SET_SPEED, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
fan.ATTR_SPEED: params['fanSpeed'] fan.ATTR_SPEED: params['fanSpeed']
}, blocking=True) }, blocking=True, context=data.context)
@register_trait @register_trait
@ -934,7 +935,7 @@ class ModesTrait(_Trait):
return response return response
async def execute(self, command, params): async def execute(self, command, data, params):
"""Execute an SetModes command.""" """Execute an SetModes command."""
settings = params.get('updateModeSettings') settings = params.get('updateModeSettings')
requested_source = settings.get( requested_source = settings.get(
@ -951,4 +952,4 @@ class ModesTrait(_Trait):
media_player.SERVICE_SELECT_SOURCE, { media_player.SERVICE_SELECT_SOURCE, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_INPUT_SOURCE: source media_player.ATTR_INPUT_SOURCE: source
}, blocking=True) }, blocking=True, context=data.context)

View File

@ -1,7 +1,7 @@
"""Test Google Smart Home.""" """Test Google Smart Home."""
import pytest import pytest
from homeassistant.core import State from homeassistant.core import State, EVENT_CALL_SERVICE
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -19,8 +19,7 @@ from tests.common import (mock_device_registry, mock_registry,
BASIC_CONFIG = helpers.Config( BASIC_CONFIG = helpers.Config(
should_expose=lambda state: True, should_expose=lambda state: True,
allow_unlock=False, allow_unlock=False
agent_user_id='test-agent',
) )
REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
@ -56,7 +55,6 @@ async def test_sync_message(hass):
config = helpers.Config( config = helpers.Config(
should_expose=lambda state: state.entity_id != 'light.not_expose', should_expose=lambda state: state.entity_id != 'light.not_expose',
allow_unlock=False, allow_unlock=False,
agent_user_id='test-agent',
entity_config={ entity_config={
'light.demo_light': { 'light.demo_light': {
const.CONF_ROOM_HINT: 'Living Room', const.CONF_ROOM_HINT: 'Living Room',
@ -68,12 +66,14 @@ async def test_sync_message(hass):
events = [] events = []
hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
result = await sh.async_handle_message(hass, config, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, config, 'test-agent',
"inputs": [{ {
"intent": "action.devices.SYNC" "requestId": REQ_ID,
}] "inputs": [{
}) "intent": "action.devices.SYNC"
}]
})
assert result == { assert result == {
'requestId': REQ_ID, 'requestId': REQ_ID,
@ -114,6 +114,7 @@ async def test_sync_message(hass):
} }
# pylint: disable=redefined-outer-name
async def test_sync_in_area(hass, registries): async def test_sync_in_area(hass, registries):
"""Test a sync message where room hint comes from area.""" """Test a sync message where room hint comes from area."""
area = registries.area.async_create("Living Room") area = registries.area.async_create("Living Room")
@ -142,19 +143,20 @@ async def test_sync_in_area(hass, registries):
config = helpers.Config( config = helpers.Config(
should_expose=lambda _: True, should_expose=lambda _: True,
allow_unlock=False, allow_unlock=False,
agent_user_id='test-agent',
entity_config={} entity_config={}
) )
events = [] events = []
hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
result = await sh.async_handle_message(hass, config, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, config, 'test-agent',
"inputs": [{ {
"intent": "action.devices.SYNC" "requestId": REQ_ID,
}] "inputs": [{
}) "intent": "action.devices.SYNC"
}]
})
assert result == { assert result == {
'requestId': REQ_ID, 'requestId': REQ_ID,
@ -216,21 +218,23 @@ async def test_query_message(hass):
events = [] events = []
hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append) hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append)
result = await sh.async_handle_message(hass, BASIC_CONFIG, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, BASIC_CONFIG, 'test-agent',
"inputs": [{ {
"intent": "action.devices.QUERY", "requestId": REQ_ID,
"payload": { "inputs": [{
"devices": [{ "intent": "action.devices.QUERY",
"id": "light.demo_light", "payload": {
}, { "devices": [{
"id": "light.another_light", "id": "light.demo_light",
}, { }, {
"id": "light.non_existing", "id": "light.another_light",
}] }, {
} "id": "light.non_existing",
}] }]
}) }
}]
})
assert result == { assert result == {
'requestId': REQ_ID, 'requestId': REQ_ID,
@ -280,39 +284,44 @@ async def test_execute(hass):
'light': {'platform': 'demo'} 'light': {'platform': 'demo'}
}) })
events = []
hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
await hass.services.async_call( await hass.services.async_call(
'light', 'turn_off', {'entity_id': 'light.ceiling_lights'}, 'light', 'turn_off', {'entity_id': 'light.ceiling_lights'},
blocking=True) blocking=True)
result = await sh.async_handle_message(hass, BASIC_CONFIG, { events = []
"requestId": REQ_ID, hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
"inputs": [{
"intent": "action.devices.EXECUTE", service_events = []
"payload": { hass.bus.async_listen(EVENT_CALL_SERVICE, service_events.append)
"commands": [{
"devices": [ result = await sh.async_handle_message(
{"id": "light.non_existing"}, hass, BASIC_CONFIG, None,
{"id": "light.ceiling_lights"}, {
], "requestId": REQ_ID,
"execution": [{ "inputs": [{
"command": "action.devices.commands.OnOff", "intent": "action.devices.EXECUTE",
"params": { "payload": {
"on": True "commands": [{
} "devices": [
}, { {"id": "light.non_existing"},
"command": {"id": "light.ceiling_lights"},
"action.devices.commands.BrightnessAbsolute", ],
"params": { "execution": [{
"brightness": 20 "command": "action.devices.commands.OnOff",
} "params": {
"on": True
}
}, {
"command":
"action.devices.commands.BrightnessAbsolute",
"params": {
"brightness": 20
}
}]
}] }]
}] }
} }]
}] })
})
assert result == { assert result == {
"requestId": REQ_ID, "requestId": REQ_ID,
@ -383,6 +392,24 @@ async def test_execute(hass):
} }
} }
assert len(service_events) == 2
assert service_events[0].data == {
'domain': 'light',
'service': 'turn_on',
'service_data': {'entity_id': 'light.ceiling_lights'}
}
assert service_events[0].context == events[2].context
assert service_events[1].data == {
'domain': 'light',
'service': 'turn_on',
'service_data': {
'brightness_pct': 20,
'entity_id': 'light.ceiling_lights'
}
}
assert service_events[1].context == events[2].context
assert service_events[1].context == events[3].context
async def test_raising_error_trait(hass): async def test_raising_error_trait(hass):
"""Test raising an error while executing a trait command.""" """Test raising an error while executing a trait command."""
@ -397,26 +424,28 @@ async def test_raising_error_trait(hass):
hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
await hass.async_block_till_done() await hass.async_block_till_done()
result = await sh.async_handle_message(hass, BASIC_CONFIG, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, BASIC_CONFIG, 'test-agent',
"inputs": [{ {
"intent": "action.devices.EXECUTE", "requestId": REQ_ID,
"payload": { "inputs": [{
"commands": [{ "intent": "action.devices.EXECUTE",
"devices": [ "payload": {
{"id": "climate.bla"}, "commands": [{
], "devices": [
"execution": [{ {"id": "climate.bla"},
"command": "action.devices.commands." ],
"ThermostatTemperatureSetpoint", "execution": [{
"params": { "command": "action.devices.commands."
"thermostatTemperatureSetpoint": 10 "ThermostatTemperatureSetpoint",
} "params": {
"thermostatTemperatureSetpoint": 10
}
}]
}] }]
}] }
} }]
}] })
})
assert result == { assert result == {
"requestId": REQ_ID, "requestId": REQ_ID,
@ -446,6 +475,7 @@ async def test_raising_error_trait(hass):
async def test_serialize_input_boolean(hass): async def test_serialize_input_boolean(hass):
"""Test serializing an input boolean entity.""" """Test serializing an input boolean entity."""
state = State('input_boolean.bla', 'on') state = State('input_boolean.bla', 'on')
# pylint: disable=protected-access
entity = sh._GoogleEntity(hass, BASIC_CONFIG, state) entity = sh._GoogleEntity(hass, BASIC_CONFIG, state)
result = await entity.sync_serialize() result = await entity.sync_serialize()
assert result == { assert result == {
@ -466,15 +496,17 @@ async def test_unavailable_state_doesnt_sync(hass):
) )
light.hass = hass light.hass = hass
light.entity_id = 'light.demo_light' light.entity_id = 'light.demo_light'
light._available = False light._available = False # pylint: disable=protected-access
await light.async_update_ha_state() await light.async_update_ha_state()
result = await sh.async_handle_message(hass, BASIC_CONFIG, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, BASIC_CONFIG, 'test-agent',
"inputs": [{ {
"intent": "action.devices.SYNC" "requestId": REQ_ID,
}] "inputs": [{
}) "intent": "action.devices.SYNC"
}]
})
assert result == { assert result == {
'requestId': REQ_ID, 'requestId': REQ_ID,
@ -495,12 +527,14 @@ async def test_empty_name_doesnt_sync(hass):
light.entity_id = 'light.demo_light' light.entity_id = 'light.demo_light'
await light.async_update_ha_state() await light.async_update_ha_state()
result = await sh.async_handle_message(hass, BASIC_CONFIG, { result = await sh.async_handle_message(
"requestId": REQ_ID, hass, BASIC_CONFIG, 'test-agent',
"inputs": [{ {
"intent": "action.devices.SYNC" "requestId": REQ_ID,
}] "inputs": [{
}) "intent": "action.devices.SYNC"
}]
})
assert result == { assert result == {
'requestId': REQ_ID, 'requestId': REQ_ID,
@ -513,11 +547,13 @@ async def test_empty_name_doesnt_sync(hass):
async def test_query_disconnect(hass): async def test_query_disconnect(hass):
"""Test a disconnect message.""" """Test a disconnect message."""
result = await sh.async_handle_message(hass, BASIC_CONFIG, { result = await sh.async_handle_message(
'inputs': [ hass, BASIC_CONFIG, 'test-agent',
{'intent': 'action.devices.DISCONNECT'} {
], 'inputs': [
'requestId': REQ_ID {'intent': 'action.devices.DISCONNECT'}
}) ],
'requestId': REQ_ID
})
assert result is None assert result is None

View File

@ -19,19 +19,25 @@ from homeassistant.components.google_assistant import trait, helpers, const
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE)
from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE
from homeassistant.util import color from homeassistant.util import color
from tests.common import async_mock_service from tests.common import async_mock_service
BASIC_CONFIG = helpers.Config( BASIC_CONFIG = helpers.Config(
should_expose=lambda state: True, should_expose=lambda state: True,
allow_unlock=False, allow_unlock=False
agent_user_id='test-agent', )
REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
BASIC_DATA = helpers.RequestData(
BASIC_CONFIG,
'test-agent',
REQ_ID,
) )
UNSAFE_CONFIG = helpers.Config( UNSAFE_CONFIG = helpers.Config(
should_expose=lambda state: True, should_expose=lambda state: True,
agent_user_id='test-agent',
allow_unlock=True, allow_unlock=True,
) )
@ -51,16 +57,28 @@ async def test_brightness_light(hass):
'brightness': 95 'brightness': 95
} }
events = []
hass.bus.async_listen(EVENT_CALL_SERVICE, events.append)
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await trt.execute(trait.COMMAND_BRIGHTNESS_ABSOLUTE, { await trt.execute(
'brightness': 50 trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
}) {'brightness': 50})
await hass.async_block_till_done()
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'light.bla', ATTR_ENTITY_ID: 'light.bla',
light.ATTR_BRIGHTNESS_PCT: 50 light.ATTR_BRIGHTNESS_PCT: 50
} }
assert len(events) == 1
assert events[0].data == {
'domain': 'light',
'service': 'turn_on',
'service_data': {'brightness_pct': 50, 'entity_id': 'light.bla'}
}
async def test_brightness_cover(hass): async def test_brightness_cover(hass):
"""Test brightness trait support for cover domain.""" """Test brightness trait support for cover domain."""
@ -79,9 +97,9 @@ async def test_brightness_cover(hass):
calls = async_mock_service( calls = async_mock_service(
hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
await trt.execute(trait.COMMAND_BRIGHTNESS_ABSOLUTE, { await trt.execute(
'brightness': 50 trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
}) {'brightness': 50})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'cover.bla', ATTR_ENTITY_ID: 'cover.bla',
@ -107,9 +125,9 @@ async def test_brightness_media_player(hass):
calls = async_mock_service( calls = async_mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET) hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET)
await trt.execute(trait.COMMAND_BRIGHTNESS_ABSOLUTE, { await trt.execute(
'brightness': 60 trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
}) {'brightness': 60})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'media_player.bla', ATTR_ENTITY_ID: 'media_player.bla',
@ -137,18 +155,18 @@ async def test_onoff_group(hass):
} }
on_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'group.bla', ATTR_ENTITY_ID: 'group.bla',
} }
off_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_OFF) off_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'group.bla', ATTR_ENTITY_ID: 'group.bla',
@ -176,9 +194,9 @@ async def test_onoff_input_boolean(hass):
} }
on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'input_boolean.bla', ATTR_ENTITY_ID: 'input_boolean.bla',
@ -186,9 +204,9 @@ async def test_onoff_input_boolean(hass):
off_calls = async_mock_service(hass, input_boolean.DOMAIN, off_calls = async_mock_service(hass, input_boolean.DOMAIN,
SERVICE_TURN_OFF) SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'input_boolean.bla', ATTR_ENTITY_ID: 'input_boolean.bla',
@ -216,18 +234,18 @@ async def test_onoff_switch(hass):
} }
on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'switch.bla', ATTR_ENTITY_ID: 'switch.bla',
} }
off_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_OFF) off_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'switch.bla', ATTR_ENTITY_ID: 'switch.bla',
@ -252,18 +270,18 @@ async def test_onoff_fan(hass):
} }
on_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'fan.bla', ATTR_ENTITY_ID: 'fan.bla',
} }
off_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_OFF) off_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'fan.bla', ATTR_ENTITY_ID: 'fan.bla',
@ -290,18 +308,18 @@ async def test_onoff_light(hass):
} }
on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'light.bla', ATTR_ENTITY_ID: 'light.bla',
} }
off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'light.bla', ATTR_ENTITY_ID: 'light.bla',
@ -329,9 +347,9 @@ async def test_onoff_cover(hass):
} }
on_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) on_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'cover.bla', ATTR_ENTITY_ID: 'cover.bla',
@ -339,9 +357,9 @@ async def test_onoff_cover(hass):
off_calls = async_mock_service(hass, cover.DOMAIN, off_calls = async_mock_service(hass, cover.DOMAIN,
cover.SERVICE_CLOSE_COVER) cover.SERVICE_CLOSE_COVER)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'cover.bla', ATTR_ENTITY_ID: 'cover.bla',
@ -369,9 +387,9 @@ async def test_onoff_media_player(hass):
} }
on_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'media_player.bla', ATTR_ENTITY_ID: 'media_player.bla',
@ -380,9 +398,9 @@ async def test_onoff_media_player(hass):
off_calls = async_mock_service(hass, media_player.DOMAIN, off_calls = async_mock_service(hass, media_player.DOMAIN,
SERVICE_TURN_OFF) SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'media_player.bla', ATTR_ENTITY_ID: 'media_player.bla',
@ -410,9 +428,9 @@ async def test_onoff_climate(hass):
} }
on_calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_ON) on_calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': True trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': True})
assert len(on_calls) == 1 assert len(on_calls) == 1
assert on_calls[0].data == { assert on_calls[0].data == {
ATTR_ENTITY_ID: 'climate.bla', ATTR_ENTITY_ID: 'climate.bla',
@ -421,9 +439,9 @@ async def test_onoff_climate(hass):
off_calls = async_mock_service(hass, climate.DOMAIN, off_calls = async_mock_service(hass, climate.DOMAIN,
SERVICE_TURN_OFF) SERVICE_TURN_OFF)
await trt_on.execute(trait.COMMAND_ONOFF, { await trt_on.execute(
'on': False trait.COMMAND_ONOFF, BASIC_DATA,
}) {'on': False})
assert len(off_calls) == 1 assert len(off_calls) == 1
assert off_calls[0].data == { assert off_calls[0].data == {
ATTR_ENTITY_ID: 'climate.bla', ATTR_ENTITY_ID: 'climate.bla',
@ -445,7 +463,8 @@ async def test_dock_vacuum(hass):
calls = async_mock_service(hass, vacuum.DOMAIN, calls = async_mock_service(hass, vacuum.DOMAIN,
vacuum.SERVICE_RETURN_TO_BASE) vacuum.SERVICE_RETURN_TO_BASE)
await trt.execute(trait.COMMAND_DOCK, {}) await trt.execute(
trait.COMMAND_DOCK, BASIC_DATA, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'vacuum.bla', ATTR_ENTITY_ID: 'vacuum.bla',
@ -469,7 +488,7 @@ async def test_startstop_vacuum(hass):
start_calls = async_mock_service(hass, vacuum.DOMAIN, start_calls = async_mock_service(hass, vacuum.DOMAIN,
vacuum.SERVICE_START) vacuum.SERVICE_START)
await trt.execute(trait.COMMAND_STARTSTOP, {'start': True}) await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': True})
assert len(start_calls) == 1 assert len(start_calls) == 1
assert start_calls[0].data == { assert start_calls[0].data == {
ATTR_ENTITY_ID: 'vacuum.bla', ATTR_ENTITY_ID: 'vacuum.bla',
@ -477,7 +496,7 @@ async def test_startstop_vacuum(hass):
stop_calls = async_mock_service(hass, vacuum.DOMAIN, stop_calls = async_mock_service(hass, vacuum.DOMAIN,
vacuum.SERVICE_STOP) vacuum.SERVICE_STOP)
await trt.execute(trait.COMMAND_STARTSTOP, {'start': False}) await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': False})
assert len(stop_calls) == 1 assert len(stop_calls) == 1
assert stop_calls[0].data == { assert stop_calls[0].data == {
ATTR_ENTITY_ID: 'vacuum.bla', ATTR_ENTITY_ID: 'vacuum.bla',
@ -485,7 +504,7 @@ async def test_startstop_vacuum(hass):
pause_calls = async_mock_service(hass, vacuum.DOMAIN, pause_calls = async_mock_service(hass, vacuum.DOMAIN,
vacuum.SERVICE_PAUSE) vacuum.SERVICE_PAUSE)
await trt.execute(trait.COMMAND_PAUSEUNPAUSE, {'pause': True}) await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': True})
assert len(pause_calls) == 1 assert len(pause_calls) == 1
assert pause_calls[0].data == { assert pause_calls[0].data == {
ATTR_ENTITY_ID: 'vacuum.bla', ATTR_ENTITY_ID: 'vacuum.bla',
@ -493,7 +512,7 @@ async def test_startstop_vacuum(hass):
unpause_calls = async_mock_service(hass, vacuum.DOMAIN, unpause_calls = async_mock_service(hass, vacuum.DOMAIN,
vacuum.SERVICE_START) vacuum.SERVICE_START)
await trt.execute(trait.COMMAND_PAUSEUNPAUSE, {'pause': False}) await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': False})
assert len(unpause_calls) == 1 assert len(unpause_calls) == 1
assert unpause_calls[0].data == { assert unpause_calls[0].data == {
ATTR_ENTITY_ID: 'vacuum.bla', ATTR_ENTITY_ID: 'vacuum.bla',
@ -532,7 +551,7 @@ async def test_color_spectrum_light(hass):
}) })
calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, { await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, {
'color': { 'color': {
'spectrumRGB': 1052927 'spectrumRGB': 1052927
} }
@ -581,14 +600,14 @@ async def test_color_temperature_light(hass):
calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
with pytest.raises(helpers.SmartHomeError) as err: with pytest.raises(helpers.SmartHomeError) as err:
await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, { await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, {
'color': { 'color': {
'temperature': 5555 'temperature': 5555
} }
}) })
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, { await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, {
'color': { 'color': {
'temperature': 2857 'temperature': 2857
} }
@ -626,7 +645,7 @@ async def test_scene_scene(hass):
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON) calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON)
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, {}) await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'scene.bla', ATTR_ENTITY_ID: 'scene.bla',
@ -643,7 +662,7 @@ async def test_scene_script(hass):
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
calls = async_mock_service(hass, script.DOMAIN, SERVICE_TURN_ON) calls = async_mock_service(hass, script.DOMAIN, SERVICE_TURN_ON)
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, {}) await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {})
# We don't wait till script execution is done. # We don't wait till script execution is done.
await hass.async_block_till_done() await hass.async_block_till_done()
@ -695,10 +714,11 @@ async def test_temperature_setting_climate_range(hass):
calls = async_mock_service( calls = async_mock_service(
hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE) hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
await trt.execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, { await trt.execute(
'thermostatTemperatureSetpointHigh': 25, trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, BASIC_DATA, {
'thermostatTemperatureSetpointLow': 20, 'thermostatTemperatureSetpointHigh': 25,
}) 'thermostatTemperatureSetpointLow': 20,
})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'climate.bla', ATTR_ENTITY_ID: 'climate.bla',
@ -708,7 +728,7 @@ async def test_temperature_setting_climate_range(hass):
calls = async_mock_service( calls = async_mock_service(
hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE)
await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, { await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {
'thermostatMode': 'heatcool', 'thermostatMode': 'heatcool',
}) })
assert len(calls) == 1 assert len(calls) == 1
@ -718,9 +738,9 @@ async def test_temperature_setting_climate_range(hass):
} }
with pytest.raises(helpers.SmartHomeError) as err: with pytest.raises(helpers.SmartHomeError) as err:
await trt.execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, { await trt.execute(
'thermostatTemperatureSetpoint': -100, trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
}) {'thermostatTemperatureSetpoint': -100})
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
hass.config.units.temperature_unit = TEMP_CELSIUS hass.config.units.temperature_unit = TEMP_CELSIUS
@ -762,13 +782,13 @@ async def test_temperature_setting_climate_setpoint(hass):
hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE) hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
with pytest.raises(helpers.SmartHomeError): with pytest.raises(helpers.SmartHomeError):
await trt.execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, { await trt.execute(
'thermostatTemperatureSetpoint': -100, trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
}) {'thermostatTemperatureSetpoint': -100})
await trt.execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, { await trt.execute(
'thermostatTemperatureSetpoint': 19, trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
}) {'thermostatTemperatureSetpoint': 19})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
ATTR_ENTITY_ID: 'climate.bla', ATTR_ENTITY_ID: 'climate.bla',
@ -793,7 +813,7 @@ async def test_lock_unlock_lock(hass):
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': True}) assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': True})
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK) calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)
await trt.execute(trait.COMMAND_LOCKUNLOCK, {'lock': True}) await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': True})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
@ -830,7 +850,7 @@ async def test_lock_unlock_unlock(hass):
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK) calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK)
await trt.execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
@ -910,7 +930,8 @@ async def test_fan_speed(hass):
trait.COMMAND_FANSPEED, params={'fanSpeed': 'medium'}) trait.COMMAND_FANSPEED, params={'fanSpeed': 'medium'})
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED) calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED)
await trt.execute(trait.COMMAND_FANSPEED, params={'fanSpeed': 'medium'}) await trt.execute(
trait.COMMAND_FANSPEED, BASIC_DATA, {'fanSpeed': 'medium'})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == { assert calls[0].data == {
@ -995,7 +1016,7 @@ async def test_modes(hass):
calls = async_mock_service( calls = async_mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE) hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE)
await trt.execute( await trt.execute(
trait.COMMAND_MODES, params={ trait.COMMAND_MODES, BASIC_DATA, {
'updateModeSettings': { 'updateModeSettings': {
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media' trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media'
}}) }})