Report scripts and groups as scenes to Alexa (#11900)
* Send Alexa Smart Home responses to debug log * Report scripts and groups as scenes to Alexa The Alexa API docs have a couple display categories that sound relevant to scenes or scripts: ACTIVITY_TRIGGER: Describes a combination of devices set to a specific state, when the state change must occur in a specific order. For example, a “watch Neflix” scene might require the: 1. TV to be powered on & 2. Input set to HDMI1. SCENE_TRIGGER: Describes a combination of devices set to a specific state, when the order of the state change is not important. For example a bedtime scene might include turning off lights and lowering the thermostat, but the order is unimportant. Additionally, Alexa has a notion of scenes that support deactivation. This is a natural fit for groups, and scripts with delays which can be cancelled. https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories The mechanism to map entities to the Alexa Discovery response is refactored since extending the data structures in MAPPING_COMPONENT to implement supportsDeactivation would have added complication to what I already found to be a confusing construct.
This commit is contained in:
parent
3aa3130d05
commit
920f9f132b
2 changed files with 382 additions and 86 deletions
|
@ -34,43 +34,268 @@ CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||||
|
|
||||||
HANDLERS = Registry()
|
HANDLERS = Registry()
|
||||||
|
|
||||||
MAPPING_COMPONENT = {
|
|
||||||
alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
class _DisplayCategory(object):
|
||||||
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
"""Possible display categories for Discovery response.
|
||||||
cover.DOMAIN: [
|
|
||||||
'DOOR', ('Alexa.PowerController',), {
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
||||||
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
|
"""
|
||||||
}
|
|
||||||
],
|
# Describes a combination of devices set to a specific state, when the
|
||||||
fan.DOMAIN: [
|
# state change must occur in a specific order. For example, a "watch
|
||||||
'OTHER', ('Alexa.PowerController',), {
|
# Neflix" scene might require the: 1. TV to be powered on & 2. Input set to
|
||||||
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
|
# HDMI1. Applies to Scenes
|
||||||
}
|
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||||
],
|
|
||||||
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
# Indicates media devices with video or photo capabilities.
|
||||||
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
CAMERA = "CAMERA"
|
||||||
light.DOMAIN: [
|
|
||||||
'LIGHT', ('Alexa.PowerController',), {
|
# Indicates a door.
|
||||||
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
|
DOOR = "DOOR"
|
||||||
light.SUPPORT_RGB_COLOR: 'Alexa.ColorController',
|
|
||||||
light.SUPPORT_XY_COLOR: 'Alexa.ColorController',
|
# Indicates light sources or fixtures.
|
||||||
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
|
LIGHT = "LIGHT"
|
||||||
}
|
|
||||||
],
|
# An endpoint that cannot be described in on of the other categories.
|
||||||
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
|
OTHER = "OTHER"
|
||||||
media_player.DOMAIN: [
|
|
||||||
'TV', ('Alexa.PowerController',), {
|
# Describes a combination of devices set to a specific state, when the
|
||||||
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
|
# order of the state change is not important. For example a bedtime scene
|
||||||
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
|
# might include turning off lights and lowering the thermostat, but the
|
||||||
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
|
# order is unimportant. Applies to Scenes
|
||||||
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
|
SCENE_TRIGGER = "SCENE_TRIGGER"
|
||||||
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
|
|
||||||
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
|
# Indicates an endpoint that locks.
|
||||||
}
|
SMARTLOCK = "SMARTLOCK"
|
||||||
],
|
|
||||||
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
|
# Indicates modules that are plugged into an existing electrical outlet.
|
||||||
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
# Can control a variety of devices.
|
||||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
SMARTPLUG = "SMARTPLUG"
|
||||||
|
|
||||||
|
# Indicates the endpoint is a speaker or speaker system.
|
||||||
|
SPEAKER = "SPEAKER"
|
||||||
|
|
||||||
|
# Indicates in-wall switches wired to the electrical system. Can control a
|
||||||
|
# variety of devices.
|
||||||
|
SWITCH = "SWITCH"
|
||||||
|
|
||||||
|
# Indicates endpoints that report the temperature only.
|
||||||
|
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
||||||
|
|
||||||
|
# Indicates endpoints that control temperature, stand-alone air
|
||||||
|
# conditioners, or heaters with direct temperature control.
|
||||||
|
THERMOSTAT = "THERMOSTAT"
|
||||||
|
|
||||||
|
# Indicates the endpoint is a television.
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
TV = "TV"
|
||||||
|
|
||||||
|
|
||||||
|
def _capability(interface,
|
||||||
|
version=3,
|
||||||
|
supports_deactivation=None,
|
||||||
|
cap_type='AlexaInterface'):
|
||||||
|
"""Return a Smart Home API capability object.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#capability-object
|
||||||
|
|
||||||
|
There are some additional fields allowed but not implemented here since
|
||||||
|
we've no use case for them yet:
|
||||||
|
|
||||||
|
- properties.supported
|
||||||
|
- proactively_reported
|
||||||
|
- retrievable
|
||||||
|
|
||||||
|
`supports_deactivation` applies only to scenes.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
'type': cap_type,
|
||||||
|
'interface': interface,
|
||||||
|
'version': version,
|
||||||
|
}
|
||||||
|
|
||||||
|
if supports_deactivation is not None:
|
||||||
|
result['supportsDeactivation'] = supports_deactivation
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class _EntityCapabilities(object):
|
||||||
|
def __init__(self, config, entity):
|
||||||
|
self.config = config
|
||||||
|
self.entity = entity
|
||||||
|
|
||||||
|
def display_categories(self):
|
||||||
|
"""Return a list of display categories."""
|
||||||
|
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
||||||
|
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
||||||
|
return [entity_conf[CONF_DISPLAY_CATEGORIES]]
|
||||||
|
return self.default_display_categories()
|
||||||
|
|
||||||
|
def default_display_categories(self):
|
||||||
|
"""Return a list of default display categories.
|
||||||
|
|
||||||
|
This can be overridden by the user in the Home Assistant configuration.
|
||||||
|
|
||||||
|
See also _DisplayCategory.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
"""Return a list of supported capabilities.
|
||||||
|
|
||||||
|
You might find _capability() useful.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class _GenericCapabilities(_EntityCapabilities):
|
||||||
|
"""A generic, on/off device.
|
||||||
|
|
||||||
|
The choice of last resort.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
return [_capability('Alexa.PowerController')]
|
||||||
|
|
||||||
|
|
||||||
|
class _SwitchCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.SWITCH]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
return [_capability('Alexa.PowerController')]
|
||||||
|
|
||||||
|
|
||||||
|
class _CoverCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.DOOR]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
capabilities = [_capability('Alexa.PowerController')]
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & cover.SUPPORT_SET_POSITION:
|
||||||
|
capabilities.append(_capability('Alexa.PercentageController'))
|
||||||
|
return capabilities
|
||||||
|
|
||||||
|
|
||||||
|
class _LightCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.LIGHT]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
capabilities = [_capability('Alexa.PowerController')]
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & light.SUPPORT_BRIGHTNESS:
|
||||||
|
capabilities.append(_capability('Alexa.BrightnessController'))
|
||||||
|
if supported & light.SUPPORT_RGB_COLOR:
|
||||||
|
capabilities.append(_capability('Alexa.ColorController'))
|
||||||
|
if supported & light.SUPPORT_XY_COLOR:
|
||||||
|
capabilities.append(_capability('Alexa.ColorController'))
|
||||||
|
if supported & light.SUPPORT_COLOR_TEMP:
|
||||||
|
capabilities.append(
|
||||||
|
_capability('Alexa.ColorTemperatureController'))
|
||||||
|
return capabilities
|
||||||
|
|
||||||
|
|
||||||
|
class _FanCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
capabilities = [_capability('Alexa.PowerController')]
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & fan.SUPPORT_SET_SPEED:
|
||||||
|
capabilities.append(_capability('Alexa.PercentageController'))
|
||||||
|
return capabilities
|
||||||
|
|
||||||
|
|
||||||
|
class _LockCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.SMARTLOCK]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
return [_capability('Alexa.LockController')]
|
||||||
|
|
||||||
|
|
||||||
|
class _MediaPlayerCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.TV]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
capabilities = [_capability('Alexa.PowerController')]
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & media_player.SUPPORT_VOLUME_SET:
|
||||||
|
capabilities.append(_capability('Alexa.Speaker'))
|
||||||
|
|
||||||
|
playback_features = (media_player.SUPPORT_PLAY |
|
||||||
|
media_player.SUPPORT_PAUSE |
|
||||||
|
media_player.SUPPORT_STOP |
|
||||||
|
media_player.SUPPORT_NEXT_TRACK |
|
||||||
|
media_player.SUPPORT_PREVIOUS_TRACK)
|
||||||
|
if supported & playback_features:
|
||||||
|
capabilities.append(_capability('Alexa.PlaybackController'))
|
||||||
|
|
||||||
|
return capabilities
|
||||||
|
|
||||||
|
|
||||||
|
class _SceneCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
return [_capability('Alexa.SceneController')]
|
||||||
|
|
||||||
|
|
||||||
|
class _ScriptCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||||
|
return [_capability('Alexa.SceneController',
|
||||||
|
supports_deactivation=can_cancel)]
|
||||||
|
|
||||||
|
|
||||||
|
class _GroupCapabilities(_EntityCapabilities):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
return [_capability('Alexa.SceneController',
|
||||||
|
supports_deactivation=True)]
|
||||||
|
|
||||||
|
|
||||||
|
class _UnknownEntityDomainError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _capabilities_for_entity(config, entity):
|
||||||
|
"""Return an _EntityCapabilities appropriate for given entity.
|
||||||
|
|
||||||
|
raises _UnknownEntityDomainError if the given domain is unsupported.
|
||||||
|
"""
|
||||||
|
if entity.domain not in _CAPABILITIES_FOR_DOMAIN:
|
||||||
|
raise _UnknownEntityDomainError()
|
||||||
|
return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity)
|
||||||
|
|
||||||
|
|
||||||
|
_CAPABILITIES_FOR_DOMAIN = {
|
||||||
|
alert.DOMAIN: _GenericCapabilities,
|
||||||
|
automation.DOMAIN: _GenericCapabilities,
|
||||||
|
cover.DOMAIN: _CoverCapabilities,
|
||||||
|
fan.DOMAIN: _FanCapabilities,
|
||||||
|
group.DOMAIN: _GroupCapabilities,
|
||||||
|
input_boolean.DOMAIN: _GenericCapabilities,
|
||||||
|
light.DOMAIN: _LightCapabilities,
|
||||||
|
lock.DOMAIN: _LockCapabilities,
|
||||||
|
media_player.DOMAIN: _MediaPlayerCapabilities,
|
||||||
|
scene.DOMAIN: _SceneCapabilities,
|
||||||
|
script.DOMAIN: _ScriptCapabilities,
|
||||||
|
switch.DOMAIN: _SwitchCapabilities,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,6 +383,7 @@ class SmartHomeView(http.HomeAssistantView):
|
||||||
|
|
||||||
response = yield from async_handle_message(
|
response = yield from async_handle_message(
|
||||||
hass, self.smart_home_config, message)
|
hass, self.smart_home_config, message)
|
||||||
|
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
||||||
return b'' if response is None else self.json(response)
|
return b'' if response is None else self.json(response)
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,9 +466,9 @@ def async_api_discovery(hass, config, request):
|
||||||
entity.entity_id)
|
entity.entity_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
try:
|
||||||
|
entity_capabilities = _capabilities_for_entity(config, entity)
|
||||||
if not class_data:
|
except _UnknownEntityDomainError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||||
|
@ -255,40 +481,16 @@ def async_api_discovery(hass, config, request):
|
||||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||||
description = scene_fmt.format(description)
|
description = scene_fmt.format(description)
|
||||||
|
|
||||||
display_categories = entity_conf.get(
|
|
||||||
CONF_DISPLAY_CATEGORIES, class_data[0])
|
|
||||||
|
|
||||||
endpoint = {
|
endpoint = {
|
||||||
'displayCategories': [display_categories],
|
'displayCategories': entity_capabilities.display_categories(),
|
||||||
'additionalApplianceDetails': {},
|
'additionalApplianceDetails': {},
|
||||||
'endpointId': entity.entity_id.replace('.', '#'),
|
'endpointId': entity.entity_id.replace('.', '#'),
|
||||||
'friendlyName': friendly_name,
|
'friendlyName': friendly_name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'manufacturerName': 'Home Assistant',
|
'manufacturerName': 'Home Assistant',
|
||||||
}
|
}
|
||||||
actions = set()
|
|
||||||
|
|
||||||
# static actions
|
endpoint['capabilities'] = entity_capabilities.capabilities()
|
||||||
if class_data[1]:
|
|
||||||
actions |= set(class_data[1])
|
|
||||||
|
|
||||||
# dynamic actions
|
|
||||||
if class_data[2]:
|
|
||||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
||||||
for feature, action_name in class_data[2].items():
|
|
||||||
if feature & supported > 0:
|
|
||||||
actions.add(action_name)
|
|
||||||
|
|
||||||
# Write action into capabilities
|
|
||||||
capabilities = []
|
|
||||||
for action in actions:
|
|
||||||
capabilities.append({
|
|
||||||
'type': 'AlexaInterface',
|
|
||||||
'interface': action,
|
|
||||||
'version': 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
endpoint['capabilities'] = capabilities
|
|
||||||
discovery_endpoints.append(endpoint)
|
discovery_endpoints.append(endpoint)
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
|
@ -321,8 +523,6 @@ def extract_entity(funct):
|
||||||
def async_api_turn_on(hass, config, request, entity):
|
def async_api_turn_on(hass, config, request, entity):
|
||||||
"""Process a turn on request."""
|
"""Process a turn on request."""
|
||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
if entity.domain == group.DOMAIN:
|
|
||||||
domain = ha.DOMAIN
|
|
||||||
|
|
||||||
service = SERVICE_TURN_ON
|
service = SERVICE_TURN_ON
|
||||||
if entity.domain == cover.DOMAIN:
|
if entity.domain == cover.DOMAIN:
|
||||||
|
@ -460,7 +660,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_increase_color_temp(hass, config, request, entity):
|
def async_api_increase_color_temp(hass, config, request, entity):
|
||||||
"""Process a increase color temperature request."""
|
"""Process an increase color temperature request."""
|
||||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||||
|
|
||||||
|
@ -477,8 +677,13 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_activate(hass, config, request, entity):
|
def async_api_activate(hass, config, request, entity):
|
||||||
"""Process a activate request."""
|
"""Process an activate request."""
|
||||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
if entity.domain == group.DOMAIN:
|
||||||
|
domain = ha.DOMAIN
|
||||||
|
else:
|
||||||
|
domain = entity.domain
|
||||||
|
|
||||||
|
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||||
ATTR_ENTITY_ID: entity.entity_id
|
ATTR_ENTITY_ID: entity.entity_id
|
||||||
}, blocking=False)
|
}, blocking=False)
|
||||||
|
|
||||||
|
@ -495,6 +700,33 @@ def async_api_activate(hass, config, request, entity):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
|
||||||
|
@extract_entity
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_api_deactivate(hass, config, request, entity):
|
||||||
|
"""Process a deactivate request."""
|
||||||
|
if entity.domain == group.DOMAIN:
|
||||||
|
domain = ha.DOMAIN
|
||||||
|
else:
|
||||||
|
domain = entity.domain
|
||||||
|
|
||||||
|
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id
|
||||||
|
}, blocking=False)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'cause': {'type': _Cause.VOICE_INTERACTION},
|
||||||
|
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api_message(
|
||||||
|
request,
|
||||||
|
name='DeactivationStarted',
|
||||||
|
namespace='Alexa.SceneController',
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -122,6 +122,9 @@ def test_discovery_request(hass):
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'script.test', 'off', {'friendly_name': "Test script"})
|
'script.test', 'off', {'friendly_name': "Test script"})
|
||||||
|
hass.states.async_set(
|
||||||
|
'script.test_2', 'off', {'friendly_name': "Test script 2",
|
||||||
|
'can_cancel': True})
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'input_boolean.test', 'off', {'friendly_name': "Test input boolean"})
|
'input_boolean.test', 'off', {'friendly_name': "Test input boolean"})
|
||||||
|
@ -169,7 +172,7 @@ def test_discovery_request(hass):
|
||||||
assert 'event' in msg
|
assert 'event' in msg
|
||||||
msg = msg['event']
|
msg = msg['event']
|
||||||
|
|
||||||
assert len(msg['payload']['endpoints']) == 15
|
assert len(msg['payload']['endpoints']) == 16
|
||||||
assert msg['header']['name'] == 'Discover.Response'
|
assert msg['header']['name'] == 'Discover.Response'
|
||||||
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
||||||
|
|
||||||
|
@ -221,11 +224,18 @@ def test_discovery_request(hass):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'script#test':
|
if appliance['endpointId'] == 'script#test':
|
||||||
assert appliance['displayCategories'][0] == "OTHER"
|
assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
|
||||||
assert appliance['friendlyName'] == "Test script"
|
assert appliance['friendlyName'] == "Test script"
|
||||||
assert len(appliance['capabilities']) == 1
|
assert len(appliance['capabilities']) == 1
|
||||||
assert appliance['capabilities'][-1]['interface'] == \
|
capability = appliance['capabilities'][-1]
|
||||||
'Alexa.PowerController'
|
assert capability['interface'] == 'Alexa.SceneController'
|
||||||
|
assert not capability['supportsDeactivation']
|
||||||
|
continue
|
||||||
|
|
||||||
|
if appliance['endpointId'] == 'script#test_2':
|
||||||
|
assert len(appliance['capabilities']) == 1
|
||||||
|
capability = appliance['capabilities'][-1]
|
||||||
|
assert capability['supportsDeactivation']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'input_boolean#test':
|
if appliance['endpointId'] == 'input_boolean#test':
|
||||||
|
@ -237,7 +247,7 @@ def test_discovery_request(hass):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'scene#test':
|
if appliance['endpointId'] == 'scene#test':
|
||||||
assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
|
assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
|
||||||
assert appliance['friendlyName'] == "Test scene"
|
assert appliance['friendlyName'] == "Test scene"
|
||||||
assert len(appliance['capabilities']) == 1
|
assert len(appliance['capabilities']) == 1
|
||||||
assert appliance['capabilities'][-1]['interface'] == \
|
assert appliance['capabilities'][-1]['interface'] == \
|
||||||
|
@ -303,11 +313,12 @@ def test_discovery_request(hass):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'group#test':
|
if appliance['endpointId'] == 'group#test':
|
||||||
assert appliance['displayCategories'][0] == "OTHER"
|
assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
|
||||||
assert appliance['friendlyName'] == "Test group"
|
assert appliance['friendlyName'] == "Test group"
|
||||||
assert len(appliance['capabilities']) == 1
|
assert len(appliance['capabilities']) == 1
|
||||||
assert appliance['capabilities'][-1]['interface'] == \
|
capability = appliance['capabilities'][-1]
|
||||||
'Alexa.PowerController'
|
assert capability['interface'] == 'Alexa.SceneController'
|
||||||
|
assert capability['supportsDeactivation'] is True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'cover#test':
|
if appliance['endpointId'] == 'cover#test':
|
||||||
|
@ -425,8 +436,8 @@ def test_api_function_not_implemented(hass):
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group',
|
@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover',
|
||||||
'input_boolean', 'light', 'script',
|
'input_boolean', 'light',
|
||||||
'switch'])
|
'switch'])
|
||||||
def test_api_turn_on(hass, domain):
|
def test_api_turn_on(hass, domain):
|
||||||
"""Test api turn on process."""
|
"""Test api turn on process."""
|
||||||
|
@ -441,9 +452,6 @@ def test_api_turn_on(hass, domain):
|
||||||
|
|
||||||
call_domain = domain
|
call_domain = domain
|
||||||
|
|
||||||
if domain == 'group':
|
|
||||||
call_domain = 'homeassistant'
|
|
||||||
|
|
||||||
if domain == 'cover':
|
if domain == 'cover':
|
||||||
call = async_mock_service(hass, call_domain, 'open_cover')
|
call = async_mock_service(hass, call_domain, 'open_cover')
|
||||||
else:
|
else:
|
||||||
|
@ -719,7 +727,7 @@ def test_api_increase_color_temp(hass, result, initial):
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@pytest.mark.parametrize("domain", ['scene'])
|
@pytest.mark.parametrize("domain", ['scene', 'group', 'script'])
|
||||||
def test_api_activate(hass, domain):
|
def test_api_activate(hass, domain):
|
||||||
"""Test api activate process."""
|
"""Test api activate process."""
|
||||||
request = get_new_request(
|
request = get_new_request(
|
||||||
|
@ -731,7 +739,12 @@ def test_api_activate(hass, domain):
|
||||||
'friendly_name': "Test {}".format(domain)
|
'friendly_name': "Test {}".format(domain)
|
||||||
})
|
})
|
||||||
|
|
||||||
call = async_mock_service(hass, domain, 'turn_on')
|
if domain == 'group':
|
||||||
|
call_domain = 'homeassistant'
|
||||||
|
else:
|
||||||
|
call_domain = domain
|
||||||
|
|
||||||
|
call = async_mock_service(hass, call_domain, 'turn_on')
|
||||||
|
|
||||||
msg = yield from smart_home.async_handle_message(
|
msg = yield from smart_home.async_handle_message(
|
||||||
hass, DEFAULT_CONFIG, request)
|
hass, DEFAULT_CONFIG, request)
|
||||||
|
@ -747,6 +760,40 @@ def test_api_activate(hass, domain):
|
||||||
assert 'timestamp' in msg['payload']
|
assert 'timestamp' in msg['payload']
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
@pytest.mark.parametrize("domain", ['group', 'script'])
|
||||||
|
def test_api_deactivate(hass, domain):
|
||||||
|
"""Test api deactivate process."""
|
||||||
|
request = get_new_request(
|
||||||
|
'Alexa.SceneController', 'Deactivate', '{}#test'.format(domain))
|
||||||
|
|
||||||
|
# setup test devices
|
||||||
|
hass.states.async_set(
|
||||||
|
'{}.test'.format(domain), 'off', {
|
||||||
|
'friendly_name': "Test {}".format(domain)
|
||||||
|
})
|
||||||
|
|
||||||
|
if domain == 'group':
|
||||||
|
call_domain = 'homeassistant'
|
||||||
|
else:
|
||||||
|
call_domain = domain
|
||||||
|
|
||||||
|
call = async_mock_service(hass, call_domain, 'turn_off')
|
||||||
|
|
||||||
|
msg = yield from smart_home.async_handle_message(
|
||||||
|
hass, DEFAULT_CONFIG, request)
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert 'event' in msg
|
||||||
|
msg = msg['event']
|
||||||
|
|
||||||
|
assert len(call) == 1
|
||||||
|
assert call[0].data['entity_id'] == '{}.test'.format(domain)
|
||||||
|
assert msg['header']['name'] == 'DeactivationStarted'
|
||||||
|
assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION'
|
||||||
|
assert 'timestamp' in msg['payload']
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_api_set_percentage_fan(hass):
|
def test_api_set_percentage_fan(hass):
|
||||||
"""Test api set percentage for fan process."""
|
"""Test api set percentage for fan process."""
|
||||||
|
@ -1160,6 +1207,23 @@ def test_entity_config(hass):
|
||||||
'Alexa.PowerController'
|
'Alexa.PowerController'
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_unsupported_domain(hass):
|
||||||
|
"""Discovery ignores entities of unknown domains."""
|
||||||
|
request = get_new_request('Alexa.Discovery', 'Discover')
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
'woz.boop', 'on', {'friendly_name': "Boop Woz"})
|
||||||
|
|
||||||
|
msg = yield from smart_home.async_handle_message(
|
||||||
|
hass, DEFAULT_CONFIG, request)
|
||||||
|
|
||||||
|
assert 'event' in msg
|
||||||
|
msg = msg['event']
|
||||||
|
|
||||||
|
assert len(msg['payload']['endpoints']) == 0
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def do_http_discovery(config, hass, test_client):
|
def do_http_discovery(config, hass, test_client):
|
||||||
"""Submit a request to the Smart Home HTTP API."""
|
"""Submit a request to the Smart Home HTTP API."""
|
||||||
|
|
Loading…
Add table
Reference in a new issue