Clean up Alexa smart home code (#24514)

* Clean up Alexa smart home code

* lint

* Lint

* Lint
This commit is contained in:
Paulus Schoutsen 2019-06-13 08:43:57 -07:00 committed by GitHub
parent 416ff10ba9
commit 7e2278f1cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3132 additions and 2776 deletions

View File

@ -5,12 +5,13 @@ import voluptuous as vol
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import entityfilter from homeassistant.helpers import entityfilter
from homeassistant.const import CONF_NAME
from . import flash_briefings, intent, smart_home from . import flash_briefings, intent, smart_home_http
from .const import ( from .const import (
CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL, CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL,
CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER, CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER,
CONF_ENTITY_CONFIG) CONF_ENTITY_CONFIG, CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -18,9 +19,9 @@ CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_SMART_HOME = 'smart_home' CONF_SMART_HOME = 'smart_home'
ALEXA_ENTITY_SCHEMA = vol.Schema({ ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string, vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string, vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(smart_home.CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
}) })
SMART_HOME_SCHEMA = vol.Schema({ SMART_HOME_SCHEMA = vol.Schema({
@ -65,6 +66,6 @@ async def async_setup(hass, config):
pass pass
else: else:
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
await smart_home.async_setup(hass, smart_home_config) await smart_home_http.async_setup(hass, smart_home_config)
return True return True

View File

@ -9,7 +9,6 @@ import async_timeout
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.util import dt from homeassistant.util import dt
from .const import DEFAULT_TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -97,7 +96,7 @@ class Auth:
try: try:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
with async_timeout.timeout(DEFAULT_TIMEOUT): with async_timeout.timeout(10):
response = await session.post(LWA_TOKEN_URI, response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS, headers=LWA_HEADERS,
data=lwa_params, data=lwa_params,

View File

@ -0,0 +1,597 @@
"""Alexa capabilities."""
from datetime import datetime
import logging
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
STATE_LOCKED,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNLOCKED,
)
import homeassistant.components.climate.const as climate
from homeassistant.components import (
light,
fan,
cover,
)
import homeassistant.util.color as color_util
from .const import (
API_TEMP_UNITS,
API_THERMOSTAT_MODES,
DATE_FORMAT,
PERCENTAGE_FAN_MAP,
)
from .errors import UnsupportedProperty
_LOGGER = logging.getLogger(__name__)
class AlexaCapibility:
"""Base class for Alexa capability interfaces.
The Smart Home Skills API defines a number of "capability interfaces",
roughly analogous to domains in Home Assistant. The supported interfaces
describe what actions can be performed on a particular device.
https://developer.amazon.com/docs/device-apis/message-guide.html
"""
def __init__(self, entity):
"""Initialize an Alexa capibility."""
self.entity = entity
def name(self):
"""Return the Alexa API name of this interface."""
raise NotImplementedError
@staticmethod
def properties_supported():
"""Return what properties this entity supports."""
return []
@staticmethod
def properties_proactively_reported():
"""Return True if properties asynchronously reported."""
return False
@staticmethod
def properties_retrievable():
"""Return True if properties can be retrieved."""
return False
@staticmethod
def get_property(name):
"""Read and return a property.
Return value should be a dict, or raise UnsupportedProperty.
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
but returning those metadata is not yet implemented.
"""
raise UnsupportedProperty(name)
@staticmethod
def supports_deactivation():
"""Applicable only to scenes."""
return None
def serialize_discovery(self):
"""Serialize according to the Discovery API."""
result = {
'type': 'AlexaInterface',
'interface': self.name(),
'version': '3',
'properties': {
'supported': self.properties_supported(),
'proactivelyReported': self.properties_proactively_reported(),
'retrievable': self.properties_retrievable(),
},
}
# pylint: disable=assignment-from-none
supports_deactivation = self.supports_deactivation()
if supports_deactivation is not None:
result['supportsDeactivation'] = supports_deactivation
return result
def serialize_properties(self):
"""Return properties serialized for an API response."""
for prop in self.properties_supported():
prop_name = prop['name']
# pylint: disable=assignment-from-no-return
prop_value = self.get_property(prop_name)
if prop_value is not None:
yield {
'name': prop_name,
'namespace': self.name(),
'value': prop_value,
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
'uncertaintyInMilliseconds': 0
}
class AlexaEndpointHealth(AlexaCapibility):
"""Implements Alexa.EndpointHealth.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
"""
def __init__(self, hass, entity):
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.EndpointHealth'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'connectivity'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return False
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'connectivity':
raise UnsupportedProperty(name)
if self.entity.state == STATE_UNAVAILABLE:
return {'value': 'UNREACHABLE'}
return {'value': 'OK'}
class AlexaPowerController(AlexaCapibility):
"""Implements Alexa.PowerController.
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PowerController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'powerState'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'powerState':
raise UnsupportedProperty(name)
if self.entity.state == STATE_OFF:
return 'OFF'
return 'ON'
class AlexaLockController(AlexaCapibility):
"""Implements Alexa.LockController.
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.LockController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'lockState'}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'lockState':
raise UnsupportedProperty(name)
if self.entity.state == STATE_LOCKED:
return 'LOCKED'
if self.entity.state == STATE_UNLOCKED:
return 'UNLOCKED'
return 'JAMMED'
class AlexaSceneController(AlexaCapibility):
"""Implements Alexa.SceneController.
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
"""
def __init__(self, entity, supports_deactivation):
"""Initialize the entity."""
super().__init__(entity)
self.supports_deactivation = lambda: supports_deactivation
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.SceneController'
class AlexaBrightnessController(AlexaCapibility):
"""Implements Alexa.BrightnessController.
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.BrightnessController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'brightness'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'brightness':
raise UnsupportedProperty(name)
if 'brightness' in self.entity.attributes:
return round(self.entity.attributes['brightness'] / 255.0 * 100)
return 0
class AlexaColorController(AlexaCapibility):
"""Implements Alexa.ColorController.
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ColorController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'color'}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'color':
raise UnsupportedProperty(name)
hue, saturation = self.entity.attributes.get(
light.ATTR_HS_COLOR, (0, 0))
return {
'hue': hue,
'saturation': saturation / 100.0,
'brightness': self.entity.attributes.get(
light.ATTR_BRIGHTNESS, 0) / 255.0,
}
class AlexaColorTemperatureController(AlexaCapibility):
"""Implements Alexa.ColorTemperatureController.
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ColorTemperatureController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'colorTemperatureInKelvin'}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'colorTemperatureInKelvin':
raise UnsupportedProperty(name)
if 'color_temp' in self.entity.attributes:
return color_util.color_temperature_mired_to_kelvin(
self.entity.attributes['color_temp'])
return 0
class AlexaPercentageController(AlexaCapibility):
"""Implements Alexa.PercentageController.
https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PercentageController'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'percentage'}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'percentage':
raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
speed = self.entity.attributes.get(fan.ATTR_SPEED)
return PERCENTAGE_FAN_MAP.get(speed, 0)
if self.entity.domain == cover.DOMAIN:
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
return 0
class AlexaSpeaker(AlexaCapibility):
"""Implements Alexa.Speaker.
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.Speaker'
class AlexaStepSpeaker(AlexaCapibility):
"""Implements Alexa.StepSpeaker.
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.StepSpeaker'
class AlexaPlaybackController(AlexaCapibility):
"""Implements Alexa.PlaybackController.
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PlaybackController'
class AlexaInputController(AlexaCapibility):
"""Implements Alexa.InputController.
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.InputController'
class AlexaTemperatureSensor(AlexaCapibility):
"""Implements Alexa.TemperatureSensor.
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
"""
def __init__(self, hass, entity):
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.TemperatureSensor'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'temperature'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'temperature':
raise UnsupportedProperty(name)
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
class AlexaContactSensor(AlexaCapibility):
"""Implements Alexa.ContactSensor.
The Alexa.ContactSensor interface describes the properties and events used
to report the state of an endpoint that detects contact between two
surfaces. For example, a contact sensor can report whether a door or window
is open.
https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html
"""
def __init__(self, hass, entity):
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ContactSensor'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'detectionState'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'detectionState':
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return 'DETECTED'
return 'NOT_DETECTED'
class AlexaMotionSensor(AlexaCapibility):
"""Implements Alexa.MotionSensor.
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
"""
def __init__(self, hass, entity):
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.MotionSensor'
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'detectionState'}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != 'detectionState':
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return 'DETECTED'
return 'NOT_DETECTED'
class AlexaThermostatController(AlexaCapibility):
"""Implements Alexa.ThermostatController.
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
"""
def __init__(self, hass, entity):
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ThermostatController'
def properties_supported(self):
"""Return what properties this entity supports."""
properties = []
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
properties.append({'name': 'targetSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
properties.append({'name': 'lowerSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
properties.append({'name': 'upperSetpoint'})
if supported & climate.SUPPORT_OPERATION_MODE:
properties.append({'name': 'thermostatMode'})
return properties
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name == 'thermostatMode':
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
mode = API_THERMOSTAT_MODES.get(ha_mode)
if mode is None:
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
self.entity.entity_id, type(self.entity),
climate.ATTR_OPERATION_MODE, ha_mode)
raise UnsupportedProperty(name)
return mode
unit = self.hass.config.units.temperature_unit
if name == 'targetSetpoint':
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
else:
raise UnsupportedProperty(name)
if temp is None:
return None
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}

View File

@ -0,0 +1,13 @@
"""Config helpers for Alexa."""
class Config:
"""Hold the configuration for Alexa."""
def __init__(self, endpoint, async_get_access_token, should_expose,
entity_config=None):
"""Initialize the configuration."""
self.endpoint = endpoint
self.async_get_access_token = async_get_access_token
self.should_expose = should_expose
self.entity_config = entity_config or {}

View File

@ -1,4 +1,15 @@
"""Constants for the Alexa integration.""" """Constants for the Alexa integration."""
from collections import OrderedDict
from homeassistant.const import (
STATE_OFF,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.components.climate import const as climate
from homeassistant.components import fan
DOMAIN = 'alexa' DOMAIN = 'alexa'
# Flash briefing constants # Flash briefing constants
@ -25,4 +36,75 @@ SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
DEFAULT_TIMEOUT = 30 API_DIRECTIVE = 'directive'
API_ENDPOINT = 'endpoint'
API_EVENT = 'event'
API_CONTEXT = 'context'
API_HEADER = 'header'
API_PAYLOAD = 'payload'
API_SCOPE = 'scope'
API_CHANGE = 'change'
CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'
AUTH_KEY = "alexa.smart_home.auth"
API_TEMP_UNITS = {
TEMP_FAHRENHEIT: 'FAHRENHEIT',
TEMP_CELSIUS: 'CELSIUS',
}
# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a
# reverse mapping of this dict and we want to map the first occurrance of OFF
# back to HA state.
API_THERMOSTAT_MODES = OrderedDict([
(climate.STATE_HEAT, 'HEAT'),
(climate.STATE_COOL, 'COOL'),
(climate.STATE_AUTO, 'AUTO'),
(climate.STATE_ECO, 'ECO'),
(climate.STATE_MANUAL, 'AUTO'),
(STATE_OFF, 'OFF'),
(climate.STATE_IDLE, 'OFF'),
(climate.STATE_FAN_ONLY, 'OFF'),
(climate.STATE_DRY, 'OFF'),
])
PERCENTAGE_FAN_MAP = {
fan.SPEED_LOW: 33,
fan.SPEED_MEDIUM: 66,
fan.SPEED_HIGH: 100,
}
class Cause:
"""Possible causes for property changes.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
"""
# Indicates that the event was caused by a customer interaction with an
# application. For example, a customer switches on a light, or locks a door
# using the Alexa app or an app provided by a device vendor.
APP_INTERACTION = 'APP_INTERACTION'
# Indicates that the event was caused by a physical interaction with an
# endpoint. For example manually switching on a light or manually locking a
# door lock
PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION'
# Indicates that the event was caused by the periodic poll of an appliance,
# which found a change in value. For example, you might poll a temperature
# sensor every hour, and send the updated temperature to Alexa.
PERIODIC_POLL = 'PERIODIC_POLL'
# Indicates that the event was caused by the application of a device rule.
# For example, a customer configures a rule to switch on a light if a
# motion sensor detects motion. In this case, Alexa receives an event from
# the motion sensor, and another event from the light to indicate that its
# state change was caused by the rule.
RULE_TRIGGER = 'RULE_TRIGGER'
# Indicates that the event was caused by a voice interaction with Alexa.
# For example a user speaking to their Echo device.
VOICE_INTERACTION = 'VOICE_INTERACTION'

View File

@ -0,0 +1,445 @@
"""Alexa entity adapters."""
from typing import List
from homeassistant.core import callback
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
CLOUD_NEVER_EXPOSED_ENTITIES,
CONF_NAME,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.util.decorator import Registry
from homeassistant.components.climate import const as climate
from homeassistant.components import (
alert, automation, binary_sensor, cover, fan, group,
input_boolean, light, lock, media_player, scene, script, sensor, switch)
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
from .capabilities import (
AlexaBrightnessController,
AlexaColorController,
AlexaColorTemperatureController,
AlexaContactSensor,
AlexaEndpointHealth,
AlexaInputController,
AlexaLockController,
AlexaMotionSensor,
AlexaPercentageController,
AlexaPlaybackController,
AlexaPowerController,
AlexaSceneController,
AlexaSpeaker,
AlexaStepSpeaker,
AlexaTemperatureSensor,
AlexaThermostatController,
)
ENTITY_ADAPTERS = Registry()
class DisplayCategory:
"""Possible display categories for Discovery response.
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
"""
# Describes a combination of devices set to a specific state, when the
# state change must occur in a specific order. For example, a "watch
# Netflix" scene might require the: 1. TV to be powered on & 2. Input set
# to HDMI1. Applies to Scenes
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
# Indicates media devices with video or photo capabilities.
CAMERA = "CAMERA"
# Indicates an endpoint that detects and reports contact.
CONTACT_SENSOR = "CONTACT_SENSOR"
# Indicates a door.
DOOR = "DOOR"
# Indicates light sources or fixtures.
LIGHT = "LIGHT"
# Indicates an endpoint that detects and reports motion.
MOTION_SENSOR = "MOTION_SENSOR"
# An endpoint that cannot be described in on of the other categories.
OTHER = "OTHER"
# 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. Applies to Scenes
SCENE_TRIGGER = "SCENE_TRIGGER"
# Indicates an endpoint that locks.
SMARTLOCK = "SMARTLOCK"
# Indicates modules that are plugged into an existing electrical outlet.
# Can control a variety of devices.
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.
TV = "TV"
class AlexaEntity:
"""An adaptation of an entity, expressed in Alexa's terms.
The API handlers should manipulate entities only through this interface.
"""
def __init__(self, hass, config, entity):
"""Initialize Alexa Entity."""
self.hass = hass
self.config = config
self.entity = entity
self.entity_conf = config.entity_config.get(entity.entity_id, {})
@property
def entity_id(self):
"""Return the Entity ID."""
return self.entity.entity_id
def friendly_name(self):
"""Return the Alexa API friendly name."""
return self.entity_conf.get(CONF_NAME, self.entity.name)
def description(self):
"""Return the Alexa API description."""
return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id)
def alexa_id(self):
"""Return the Alexa API entity id."""
return self.entity.entity_id.replace('.', '#')
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 get_interface(self, capability):
"""Return the given AlexaInterface.
Raises _UnsupportedInterface.
"""
pass
def interfaces(self):
"""Return a list of supported interfaces.
Used for discovery. The list should contain AlexaInterface instances.
If the list is empty, this entity will not be discovered.
"""
raise NotImplementedError
def serialize_properties(self):
"""Yield each supported property in API format."""
for interface in self.interfaces():
for prop in interface.serialize_properties():
yield prop
@callback
def async_get_entities(hass, config) -> List[AlexaEntity]:
"""Return all entities that are supported by Alexa."""
entities = []
for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue
if state.domain not in ENTITY_ADAPTERS:
continue
alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state)
if not list(alexa_entity.interfaces()):
continue
entities.append(alexa_entity)
return entities
@ENTITY_ADAPTERS.register(alert.DOMAIN)
@ENTITY_ADAPTERS.register(automation.DOMAIN)
@ENTITY_ADAPTERS.register(group.DOMAIN)
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
class GenericCapabilities(AlexaEntity):
"""A generic, on/off device.
The choice of last resort.
"""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.OTHER]
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(switch.DOMAIN)
class SwitchCapabilities(AlexaEntity):
"""Class to represent Switch capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.SWITCH]
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
class ClimateCapabilities(AlexaEntity):
"""Class to represent Climate capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.THERMOSTAT]
def interfaces(self):
"""Yield the supported interfaces."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_ON_OFF:
yield AlexaPowerController(self.entity)
yield AlexaThermostatController(self.hass, self.entity)
yield AlexaTemperatureSensor(self.hass, self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
class CoverCapabilities(AlexaEntity):
"""Class to represent Cover capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.DOOR]
def interfaces(self):
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & cover.SUPPORT_SET_POSITION:
yield AlexaPercentageController(self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(light.DOMAIN)
class LightCapabilities(AlexaEntity):
"""Class to represent Light capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.LIGHT]
def interfaces(self):
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS:
yield AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_COLOR:
yield AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP:
yield AlexaColorTemperatureController(self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(fan.DOMAIN)
class FanCapabilities(AlexaEntity):
"""Class to represent Fan capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.OTHER]
def interfaces(self):
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED:
yield AlexaPercentageController(self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(lock.DOMAIN)
class LockCapabilities(AlexaEntity):
"""Class to represent Lock capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.SMARTLOCK]
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaLockController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
class MediaPlayerCapabilities(AlexaEntity):
"""Class to represent MediaPlayer capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.TV]
def interfaces(self):
"""Yield the supported interfaces."""
yield AlexaEndpointHealth(self.hass, self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.const.SUPPORT_VOLUME_SET:
yield AlexaSpeaker(self.entity)
power_features = (media_player.SUPPORT_TURN_ON |
media_player.SUPPORT_TURN_OFF)
if supported & power_features:
yield AlexaPowerController(self.entity)
step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE |
media_player.const.SUPPORT_VOLUME_STEP)
if supported & step_volume_features:
yield AlexaStepSpeaker(self.entity)
playback_features = (media_player.const.SUPPORT_PLAY |
media_player.const.SUPPORT_PAUSE |
media_player.const.SUPPORT_STOP |
media_player.const.SUPPORT_NEXT_TRACK |
media_player.const.SUPPORT_PREVIOUS_TRACK)
if supported & playback_features:
yield AlexaPlaybackController(self.entity)
if supported & media_player.SUPPORT_SELECT_SOURCE:
yield AlexaInputController(self.entity)
@ENTITY_ADAPTERS.register(scene.DOMAIN)
class SceneCapabilities(AlexaEntity):
"""Class to represent Scene capabilities."""
def description(self):
"""Return the description of the entity."""
# Required description as per Amazon Scene docs
scene_fmt = '{} (Scene connected via Home Assistant)'
return scene_fmt.format(AlexaEntity.description(self))
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.SCENE_TRIGGER]
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaSceneController(self.entity,
supports_deactivation=False)]
@ENTITY_ADAPTERS.register(script.DOMAIN)
class ScriptCapabilities(AlexaEntity):
"""Class to represent Script capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.ACTIVITY_TRIGGER]
def interfaces(self):
"""Yield the supported interfaces."""
can_cancel = bool(self.entity.attributes.get('can_cancel'))
return [AlexaSceneController(self.entity,
supports_deactivation=can_cancel)]
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
class SensorCapabilities(AlexaEntity):
"""Class to represent Sensor capabilities."""
def default_display_categories(self):
"""Return the display categories for this entity."""
# although there are other kinds of sensors, all but temperature
# sensors are currently ignored.
return [DisplayCategory.TEMPERATURE_SENSOR]
def interfaces(self):
"""Yield the supported interfaces."""
attrs = self.entity.attributes
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
):
yield AlexaTemperatureSensor(self.hass, self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
class BinarySensorCapabilities(AlexaEntity):
"""Class to represent BinarySensor capabilities."""
TYPE_CONTACT = 'contact'
TYPE_MOTION = 'motion'
def default_display_categories(self):
"""Return the display categories for this entity."""
sensor_type = self.get_type()
if sensor_type is self.TYPE_CONTACT:
return [DisplayCategory.CONTACT_SENSOR]
if sensor_type is self.TYPE_MOTION:
return [DisplayCategory.MOTION_SENSOR]
def interfaces(self):
"""Yield the supported interfaces."""
sensor_type = self.get_type()
if sensor_type is self.TYPE_CONTACT:
yield AlexaContactSensor(self.hass, self.entity)
elif sensor_type is self.TYPE_MOTION:
yield AlexaMotionSensor(self.hass, self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
def get_type(self):
"""Return the type of binary sensor."""
attrs = self.entity.attributes
if attrs.get(ATTR_DEVICE_CLASS) in (
'door',
'garage_door',
'opening',
'window',
):
return self.TYPE_CONTACT
if attrs.get(ATTR_DEVICE_CLASS) == 'motion':
return self.TYPE_MOTION

View File

@ -0,0 +1,87 @@
"""Alexa related errors."""
from homeassistant.exceptions import HomeAssistantError
from .const import API_TEMP_UNITS
class UnsupportedInterface(HomeAssistantError):
"""This entity does not support the requested Smart Home API interface."""
class UnsupportedProperty(HomeAssistantError):
"""This entity does not support the requested Smart Home API property."""
class AlexaError(Exception):
"""Base class for errors that can be serialized by the Alexa API.
A handler can raise subclasses of this to return an error to the request.
"""
namespace = None
error_type = None
def __init__(self, error_message, payload=None):
"""Initialize an alexa error."""
Exception.__init__(self)
self.error_message = error_message
self.payload = None
class AlexaInvalidEndpointError(AlexaError):
"""The endpoint in the request does not exist."""
namespace = 'Alexa'
error_type = 'NO_SUCH_ENDPOINT'
def __init__(self, endpoint_id):
"""Initialize invalid endpoint error."""
msg = 'The endpoint {} does not exist'.format(endpoint_id)
AlexaError.__init__(self, msg)
self.endpoint_id = endpoint_id
class AlexaInvalidValueError(AlexaError):
"""Class to represent InvalidValue errors."""
namespace = 'Alexa'
error_type = 'INVALID_VALUE'
class AlexaUnsupportedThermostatModeError(AlexaError):
"""Class to represent UnsupportedThermostatMode errors."""
namespace = 'Alexa.ThermostatController'
error_type = 'UNSUPPORTED_THERMOSTAT_MODE'
class AlexaTempRangeError(AlexaError):
"""Class to represent TempRange errors."""
namespace = 'Alexa'
error_type = 'TEMPERATURE_VALUE_OUT_OF_RANGE'
def __init__(self, hass, temp, min_temp, max_temp):
"""Initialize TempRange error."""
unit = hass.config.units.temperature_unit
temp_range = {
'minimumValue': {
'value': min_temp,
'scale': API_TEMP_UNITS[unit],
},
'maximumValue': {
'value': max_temp,
'scale': API_TEMP_UNITS[unit],
},
}
payload = {'validRange': temp_range}
msg = 'The requested temperature {} is out of range'.format(temp)
AlexaError.__init__(self, msg, payload)
class AlexaBridgeUnreachableError(AlexaError):
"""Class to represent BridgeUnreachable errors."""
namespace = 'Alexa'
error_type = 'BRIDGE_UNREACHABLE'

View File

@ -0,0 +1,728 @@
"""Alexa message handlers."""
from datetime import datetime
import logging
import math
from homeassistant import core as ha
from homeassistant.util.decorator import Registry
import homeassistant.util.color as color_util
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_UNLOCK,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.components.climate import const as climate
from homeassistant.components import cover, fan, group, light, media_player
from homeassistant.util.temperature import convert as convert_temperature
from .const import (
AUTH_KEY,
API_TEMP_UNITS,
API_THERMOSTAT_MODES,
Cause,
)
from .entities import async_get_entities
from .state_report import async_enable_proactive_mode
from .errors import (
AlexaInvalidValueError,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
)
_LOGGER = logging.getLogger(__name__)
HANDLERS = Registry()
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
async def async_api_discovery(hass, config, directive, context):
"""Create a API formatted discovery response.
Async friendly.
"""
discovery_endpoints = [
{
'displayCategories': alexa_entity.display_categories(),
'cookie': {},
'endpointId': alexa_entity.alexa_id(),
'friendlyName': alexa_entity.friendly_name(),
'description': alexa_entity.description(),
'manufacturerName': 'Home Assistant',
'capabilities': [
i.serialize_discovery() for i in alexa_entity.interfaces()
]
}
for alexa_entity in async_get_entities(hass, config)
if config.should_expose(alexa_entity.entity_id)
]
return directive.response(
name='Discover.Response',
namespace='Alexa.Discovery',
payload={'endpoints': discovery_endpoints},
)
@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
async def async_api_accept_grant(hass, config, directive, context):
"""Create a API formatted AcceptGrant response.
Async friendly.
"""
auth_code = directive.payload['grant']['code']
_LOGGER.debug("AcceptGrant code: %s", auth_code)
if AUTH_KEY in hass.data:
await hass.data[AUTH_KEY].async_do_auth(auth_code)
await async_enable_proactive_mode(hass, config)
return directive.response(
name='AcceptGrant.Response',
namespace='Alexa.Authorization',
payload={})
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
async def async_api_turn_on(hass, config, directive, context):
"""Process a turn on request."""
entity = directive.entity
domain = entity.domain
if domain == group.DOMAIN:
domain = ha.DOMAIN
service = SERVICE_TURN_ON
if domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
async def async_api_turn_off(hass, config, directive, context):
"""Process a turn off request."""
entity = directive.entity
domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
service = SERVICE_TURN_OFF
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
async def async_api_set_brightness(hass, config, directive, context):
"""Process a set brightness request."""
entity = directive.entity
brightness = int(directive.payload['brightness'])
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
async def async_api_adjust_brightness(hass, config, directive, context):
"""Process an adjust brightness request."""
entity = directive.entity
brightness_delta = int(directive.payload['brightnessDelta'])
# read current state
try:
current = math.floor(
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
except ZeroDivisionError:
current = 0
# set brightness
brightness = max(0, brightness_delta + current)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
async def async_api_set_color(hass, config, directive, context):
"""Process a set color request."""
entity = directive.entity
rgb = color_util.color_hsb_to_RGB(
float(directive.payload['color']['hue']),
float(directive.payload['color']['saturation']),
float(directive.payload['color']['brightness'])
)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
async def async_api_set_color_temperature(hass, config, directive, context):
"""Process a set color temperature request."""
entity = directive.entity
kelvin = int(directive.payload['colorTemperatureInKelvin'])
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_KELVIN: kelvin,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
async def async_api_decrease_color_temp(hass, config, directive, context):
"""Process a decrease color temperature request."""
entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
value = min(max_mireds, current + 50)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
async def async_api_increase_color_temp(hass, config, directive, context):
"""Process an increase color temperature request."""
entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
value = max(min_mireds, current - 50)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
async def async_api_activate(hass, config, directive, context):
"""Process an activate request."""
entity = directive.entity
domain = entity.domain
await hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
payload = {
'cause': {'type': Cause.VOICE_INTERACTION},
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
}
return directive.response(
name='ActivationStarted',
namespace='Alexa.SceneController',
payload=payload,
)
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
async def async_api_deactivate(hass, config, directive, context):
"""Process a deactivate request."""
entity = directive.entity
domain = entity.domain
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
payload = {
'cause': {'type': Cause.VOICE_INTERACTION},
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
}
return directive.response(
name='DeactivationStarted',
namespace='Alexa.SceneController',
payload=payload,
)
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
async def async_api_set_percentage(hass, config, directive, context):
"""Process a set percentage request."""
entity = directive.entity
percentage = int(directive.payload['percentage'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
data[cover.ATTR_POSITION] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
async def async_api_adjust_percentage(hass, config, directive, context):
"""Process an adjust percentage request."""
entity = directive.entity
percentage_delta = int(directive.payload['percentageDelta'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
service = fan.SERVICE_SET_SPEED
speed = entity.attributes.get(fan.ATTR_SPEED)
if speed == "off":
current = 0
elif speed == "low":
current = 33
elif speed == "medium":
current = 66
elif speed == "high":
current = 100
# set percentage
percentage = max(0, percentage_delta + current)
speed = "off"
if percentage <= 33:
speed = "low"
elif percentage <= 66:
speed = "medium"
elif percentage <= 100:
speed = "high"
data[fan.ATTR_SPEED] = speed
elif entity.domain == cover.DOMAIN:
service = SERVICE_SET_COVER_POSITION
current = entity.attributes.get(cover.ATTR_POSITION)
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.LockController', 'Lock'))
async def async_api_lock(hass, config, directive, context):
"""Process a lock request."""
entity = directive.entity
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
response = directive.response()
response.add_context_property({
'name': 'lockState',
'namespace': 'Alexa.LockController',
'value': 'LOCKED'
})
return response
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
async def async_api_unlock(hass, config, directive, context):
"""Process an unlock request."""
entity = directive.entity
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
async def async_api_set_volume(hass, config, directive, context):
"""Process a set volume request."""
volume = round(float(directive.payload['volume'] / 100), 2)
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
async def async_api_select_input(hass, config, directive, context):
"""Process a set input request."""
media_input = directive.payload['input']
entity = directive.entity
# attempt to map the ALL UPPERCASE payload name to a source
source_list = entity.attributes[
media_player.const.ATTR_INPUT_SOURCE_LIST] or []
for source in source_list:
# response will always be space separated, so format the source in the
# most likely way to find a match
formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
if formatted_source in media_input.lower():
media_input = source
break
else:
msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id)
raise AlexaInvalidValueError(msg)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_INPUT_SOURCE: media_input,
}
await hass.services.async_call(
entity.domain, media_player.SERVICE_SELECT_SOURCE,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
async def async_api_adjust_volume(hass, config, directive, context):
"""Process an adjust volume request."""
volume_delta = int(directive.payload['volume'])
entity = directive.entity
current_level = entity.attributes.get(
media_player.const.ATTR_MEDIA_VOLUME_LEVEL)
# read current state
try:
current = math.floor(int(current_level * 100))
except ZeroDivisionError:
current = 0
volume = float(max(0, volume_delta + current) / 100)
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
async def async_api_adjust_volume_step(hass, config, directive, context):
"""Process an adjust volume step request."""
# media_player volume up/down service does not support specifying steps
# each component handles it differently e.g. via config.
# For now we use the volumeSteps returned to figure out if we
# should step up/down
volume_step = directive.payload['volumeSteps']
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id,
}
if volume_step > 0:
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_UP,
data, blocking=False, context=context)
elif volume_step < 0:
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_DOWN,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
async def async_api_set_mute(hass, config, directive, context):
"""Process a set mute request."""
mute = bool(directive.payload['mute'])
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_MUTE,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
async def async_api_play(hass, config, directive, context):
"""Process a play request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
async def async_api_pause(hass, config, directive, context):
"""Process a pause request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
async def async_api_stop(hass, config, directive, context):
"""Process a stop request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
async def async_api_next(hass, config, directive, context):
"""Process a next request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
data, blocking=False, context=context)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
async def async_api_previous(hass, config, directive, context):
"""Process a previous request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=False, context=context)
return directive.response()
def temperature_from_object(hass, temp_obj, interval=False):
"""Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
if temp_obj['scale'] == 'FAHRENHEIT':
from_unit = TEMP_FAHRENHEIT
elif temp_obj['scale'] == 'KELVIN':
# convert to Celsius if absolute temperature
if not interval:
temp -= 273.15
return convert_temperature(temp, from_unit, to_unit, interval)
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
async def async_api_set_target_temp(hass, config, directive, context):
"""Process a set target temperature request."""
entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
unit = hass.config.units.temperature_unit
data = {
ATTR_ENTITY_ID: entity.entity_id
}
payload = directive.payload
response = directive.response()
if 'targetSetpoint' in payload:
temp = temperature_from_object(hass, payload['targetSetpoint'])
if temp < min_temp or temp > max_temp:
raise AlexaTempRangeError(hass, temp, min_temp, max_temp)
data[ATTR_TEMPERATURE] = temp
response.add_context_property({
'name': 'targetSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp, 'scale': API_TEMP_UNITS[unit]},
})
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
if temp_low < min_temp or temp_low > max_temp:
raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
response.add_context_property({
'name': 'lowerSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp_low, 'scale': API_TEMP_UNITS[unit]},
})
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
if temp_high < min_temp or temp_high > max_temp:
raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
response.add_context_property({
'name': 'upperSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp_high, 'scale': API_TEMP_UNITS[unit]},
})
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
return response
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
async def async_api_adjust_target_temp(hass, config, directive, context):
"""Process an adjust target temperature request."""
entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object(
hass, directive.payload['targetSetpointDelta'], interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
data = {
ATTR_ENTITY_ID: entity.entity_id,
ATTR_TEMPERATURE: target_temp,
}
response = directive.response()
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
response.add_context_property({
'name': 'targetSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': target_temp, 'scale': API_TEMP_UNITS[unit]},
})
return response
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
async def async_api_set_thermostat_mode(hass, config, directive, context):
"""Process a set thermostat mode request."""
entity = directive.entity
mode = directive.payload['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None
)
if ha_mode not in operation_list:
msg = 'The requested thermostat mode {} is not supported'.format(mode)
raise AlexaUnsupportedThermostatModeError(msg)
data = {
ATTR_ENTITY_ID: entity.entity_id,
climate.ATTR_OPERATION_MODE: ha_mode,
}
response = directive.response()
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False, context=context)
response.add_context_property({
'name': 'thermostatMode',
'namespace': 'Alexa.ThermostatController',
'value': mode,
})
return response
@HANDLERS.register(('Alexa', 'ReportState'))
async def async_api_reportstate(hass, config, directive, context):
"""Process a ReportState request."""
return directive.response(name='StateReport')

View File

@ -0,0 +1,200 @@
"""Alexa models."""
import logging
from uuid import uuid4
from .const import (
API_CONTEXT,
API_DIRECTIVE,
API_ENDPOINT,
API_EVENT,
API_HEADER,
API_PAYLOAD,
API_SCOPE,
)
from .entities import ENTITY_ADAPTERS
from .errors import AlexaInvalidEndpointError
_LOGGER = logging.getLogger(__name__)
class AlexaDirective:
"""An incoming Alexa directive."""
def __init__(self, request):
"""Initialize a directive."""
self._directive = request[API_DIRECTIVE]
self.namespace = self._directive[API_HEADER]['namespace']
self.name = self._directive[API_HEADER]['name']
self.payload = self._directive[API_PAYLOAD]
self.has_endpoint = API_ENDPOINT in self._directive
self.entity = self.entity_id = self.endpoint = None
def load_entity(self, hass, config):
"""Set attributes related to the entity for this request.
Sets these attributes when self.has_endpoint is True:
- entity
- entity_id
- endpoint
Behavior when self.has_endpoint is False is undefined.
Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistant.
"""
_endpoint_id = self._directive[API_ENDPOINT]['endpointId']
self.entity_id = _endpoint_id.replace('#', '.')
self.entity = hass.states.get(self.entity_id)
if not self.entity:
raise AlexaInvalidEndpointError(_endpoint_id)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](
hass, config, self.entity)
def response(self,
name='Response',
namespace='Alexa',
payload=None):
"""Create an API formatted response.
Async friendly.
"""
response = AlexaResponse(name, namespace, payload)
token = self._directive[API_HEADER].get('correlationToken')
if token:
response.set_correlation_token(token)
if self.has_endpoint:
response.set_endpoint(self._directive[API_ENDPOINT].copy())
return response
def error(
self,
namespace='Alexa',
error_type='INTERNAL_ERROR',
error_message="",
payload=None
):
"""Create a API formatted error response.
Async friendly.
"""
payload = payload or {}
payload['type'] = error_type
payload['message'] = error_message
_LOGGER.info("Request %s/%s error %s: %s",
self._directive[API_HEADER]['namespace'],
self._directive[API_HEADER]['name'],
error_type, error_message)
return self.response(
name='ErrorResponse',
namespace=namespace,
payload=payload
)
class AlexaResponse:
"""Class to hold a response."""
def __init__(self, name, namespace, payload=None):
"""Initialize the response."""
payload = payload or {}
self._response = {
API_EVENT: {
API_HEADER: {
'namespace': namespace,
'name': name,
'messageId': str(uuid4()),
'payloadVersion': '3',
},
API_PAYLOAD: payload,
}
}
@property
def name(self):
"""Return the name of this response."""
return self._response[API_EVENT][API_HEADER]['name']
@property
def namespace(self):
"""Return the namespace of this response."""
return self._response[API_EVENT][API_HEADER]['namespace']
def set_correlation_token(self, token):
"""Set the correlationToken.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_HEADER]['correlationToken'] = token
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
"""Set the endpoint dictionary.
This is used to send proactive messages to Alexa.
"""
self._response[API_EVENT][API_ENDPOINT] = {
API_SCOPE: {
'type': 'BearerToken',
'token': bearer_token
}
}
if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id
if cookie is not None:
self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie
def set_endpoint(self, endpoint):
"""Set the endpoint.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_ENDPOINT] = endpoint
def _properties(self):
context = self._response.setdefault(API_CONTEXT, {})
return context.setdefault('properties', [])
def add_context_property(self, prop):
"""Add a property to the response context.
The Alexa response includes a list of properties which provides
feedback on how states have changed. For example if a user asks,
"Alexa, set theromstat to 20 degrees", the API expects a response with
the new value of the property, and Alexa will respond to the user
"Thermostat set to 20 degrees".
async_handle_message() will call .merge_context_properties() for every
request automatically, however often handlers will call services to
change state but the effects of those changes are applied
asynchronously. Thus, handlers should call this method to confirm
changes before returning.
"""
self._properties().append(prop)
def merge_context_properties(self, endpoint):
"""Add all properties from given endpoint if not already set.
Handlers should be using .add_context_property().
"""
properties = self._properties()
already_set = {(p['namespace'], p['name']) for p in properties}
for prop in endpoint.serialize_properties():
if (prop['namespace'], prop['name']) not in already_set:
self.add_context_property(prop)
def serialize(self):
"""Return response as a JSON-able data structure."""
return self._response

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
"""Alexa HTTP interface."""
import logging
from homeassistant import core
from homeassistant.components.http.view import HomeAssistantView
from .auth import Auth
from .config import Config
from .const import (
AUTH_KEY,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_ENDPOINT,
CONF_ENTITY_CONFIG,
CONF_FILTER
)
from .state_report import async_enable_proactive_mode
from .smart_home import async_handle_message
_LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
async def async_setup(hass, config):
"""Activate Smart Home functionality of Alexa component.
This is optional, triggered by having a `smart_home:` sub-section in the
alexa configuration.
Even if that's disabled, the functionality in this module may still be used
by the cloud component which will call async_handle_message directly.
"""
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
hass.data[AUTH_KEY] = Auth(hass, config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET])
async_get_access_token = \
hass.data[AUTH_KEY].async_get_access_token if AUTH_KEY in hass.data \
else None
smart_home_config = Config(
endpoint=config.get(CONF_ENDPOINT),
async_get_access_token=async_get_access_token,
should_expose=config[CONF_FILTER],
entity_config=config.get(CONF_ENTITY_CONFIG),
)
hass.http.register_view(SmartHomeView(smart_home_config))
if AUTH_KEY in hass.data:
await async_enable_proactive_mode(hass, smart_home_config)
class SmartHomeView(HomeAssistantView):
"""Expose Smart Home v3 payload interface via HTTP POST."""
url = SMART_HOME_HTTP_ENDPOINT
name = 'api:alexa:smart_home'
def __init__(self, smart_home_config):
"""Initialize."""
self.smart_home_config = smart_home_config
async def post(self, request):
"""Handle Alexa Smart Home requests.
The Smart Home API requires the endpoint to be implemented in AWS
Lambda, which will need to forward the requests to here and pass back
the response.
"""
hass = request.app['hass']
user = request['hass_user']
message = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
response = await async_handle_message(
hass, self.smart_home_config, message,
context=core.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
return b'' if response is None else self.json(response)

View File

@ -0,0 +1,109 @@
"""Alexa state report code."""
import asyncio
import json
import logging
import aiohttp
import async_timeout
from homeassistant.const import MATCH_ALL
from .const import API_CHANGE, Cause
from .entities import ENTITY_ADAPTERS
from .messages import AlexaResponse
_LOGGER = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 10
async def async_enable_proactive_mode(hass, smart_home_config):
"""Enable the proactive mode.
Proactive mode makes this component report state changes to Alexa.
"""
if smart_home_config.async_get_access_token is None:
# no function to call to get token
return
if await smart_home_config.async_get_access_token() is None:
# not ready yet
return
async def async_entity_state_listener(changed_entity, old_state,
new_state):
if not smart_home_config.should_expose(changed_entity):
_LOGGER.debug("Not exposing %s because filtered by config",
changed_entity)
return
if new_state.domain not in ENTITY_ADAPTERS:
return
alexa_changed_entity = \
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
new_state)
for interface in alexa_changed_entity.interfaces():
if interface.properties_proactively_reported():
await async_send_changereport_message(hass, smart_home_config,
alexa_changed_entity)
return
hass.helpers.event.async_track_state_change(
MATCH_ALL, async_entity_state_listener
)
async def async_send_changereport_message(hass, config, alexa_entity):
"""Send a ChangeReport message for an Alexa entity."""
token = await config.async_get_access_token()
if not token:
_LOGGER.error("Invalid access token.")
return
headers = {
"Authorization": "Bearer {}".format(token)
}
endpoint = alexa_entity.alexa_id()
# this sends all the properties of the Alexa Entity, whether they have
# changed or not. this should be improved, and properties that have not
# changed should be moved to the 'context' object
properties = list(alexa_entity.serialize_properties())
payload = {
API_CHANGE: {
'cause': {'type': Cause.APP_INTERACTION},
'properties': properties
}
}
message = AlexaResponse(name='ChangeReport', namespace='Alexa',
payload=payload)
message.set_endpoint_full(token, endpoint)
message_serialized = message.serialize()
try:
session = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(config.endpoint,
headers=headers,
json=message_serialized,
allow_redirects=True)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout calling LWA to get auth token.")
return None
response_text = await response.text()
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status != 202:
response_json = json.loads(response_text)
_LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"])

View File

@ -4,7 +4,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import const as alexa_const
from homeassistant.components.google_assistant import const as ga_c from homeassistant.components.google_assistant import const as ga_c
from homeassistant.const import ( from homeassistant.const import (
CONF_MODE, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START, CONF_MODE, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START,
@ -33,9 +33,9 @@ SERVICE_REMOTE_DISCONNECT = 'remote_disconnect'
ALEXA_ENTITY_SCHEMA = vol.Schema({ ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string, vol.Optional(alexa_const.CONF_DESCRIPTION): cv.string,
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string, vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(alexa_sh.CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
}) })
GOOGLE_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({

View File

@ -7,7 +7,10 @@ import aiohttp
from hass_nabucasa.client import CloudClient as Interface from hass_nabucasa.client import CloudClient as Interface
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import (
config as alexa_config,
smart_home as alexa_sh,
)
from homeassistant.components.google_assistant import ( from homeassistant.components.google_assistant import (
helpers as ga_h, smart_home as ga) helpers as ga_h, smart_home as ga)
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
@ -28,12 +31,12 @@ class CloudClient(Interface):
def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences, def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences,
websession: aiohttp.ClientSession, websession: aiohttp.ClientSession,
alexa_config: Dict[str, Any], google_config: Dict[str, Any]): alexa_cfg: Dict[str, Any], google_config: Dict[str, Any]):
"""Initialize client interface to Cloud.""" """Initialize client interface to Cloud."""
self._hass = hass self._hass = hass
self._prefs = prefs self._prefs = prefs
self._websession = websession self._websession = websession
self._alexa_user_config = alexa_config self._alexa_user_config = alexa_cfg
self._google_user_config = google_config self._google_user_config = google_config
self._alexa_config = None self._alexa_config = None
@ -75,12 +78,12 @@ class CloudClient(Interface):
return self._prefs.remote_enabled return self._prefs.remote_enabled
@property @property
def alexa_config(self) -> alexa_sh.Config: def alexa_config(self) -> alexa_config.Config:
"""Return Alexa config.""" """Return Alexa config."""
if not self._alexa_config: if not self._alexa_config:
alexa_conf = self._alexa_user_config alexa_conf = self._alexa_user_config
self._alexa_config = alexa_sh.Config( self._alexa_config = alexa_config.Config(
endpoint=None, endpoint=None,
async_get_access_token=None, async_get_access_token=None,
should_expose=alexa_conf[CONF_FILTER], should_expose=alexa_conf[CONF_FILTER],

View File

@ -13,7 +13,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import ( from homeassistant.components.http.data_validator import (
RequestDataValidator) RequestDataValidator)
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import entities as alexa_entities
from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.google_assistant import helpers as google_helpers
from .const import ( from .const import (
@ -421,7 +421,7 @@ def _account_data(cloud):
'prefs': client.prefs.as_dict(), 'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config, 'google_entities': client.google_user_config['filter'].config,
'alexa_entities': client.alexa_config.should_expose.config, 'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'alexa_domains': list(alexa_entities.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain, 'remote_domain': remote.instance_domain,
'remote_connected': remote.is_connected, 'remote_connected': remote.is_connected,
'remote_certificate': certificate, 'remote_certificate': certificate,
@ -497,7 +497,7 @@ async def google_assistant_list(hass, connection, msg):
vol.Optional('disable_2fa'): bool, vol.Optional('disable_2fa'): bool,
}) })
async def google_assistant_update(hass, connection, msg): async def google_assistant_update(hass, connection, msg):
"""List all google assistant entities.""" """Update google assistant config."""
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
changes = dict(msg) changes = dict(msg)
changes.pop('type') changes.pop('type')

View File

@ -1 +1,179 @@
"""Tests for the Alexa integration.""" """Tests for the Alexa integration."""
from uuid import uuid4
from homeassistant.core import Context
from homeassistant.components.alexa import config, smart_home
from tests.common import async_mock_service
TEST_URL = "https://api.amazonalexa.com/v3/events"
TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
async def get_access_token():
"""Return a test access token."""
return "thisisnotanacesstoken"
DEFAULT_CONFIG = config.Config(
endpoint=TEST_URL,
async_get_access_token=get_access_token,
should_expose=lambda entity_id: True)
def get_new_request(namespace, name, endpoint=None):
"""Generate a new API message."""
raw_msg = {
'directive': {
'header': {
'namespace': namespace,
'name': name,
'messageId': str(uuid4()),
'correlationToken': str(uuid4()),
'payloadVersion': '3',
},
'endpoint': {
'scope': {
'type': 'BearerToken',
'token': str(uuid4()),
},
'endpointId': endpoint,
},
'payload': {},
}
}
if not endpoint:
raw_msg['directive'].pop('endpoint')
return raw_msg
async def assert_request_calls_service(
namespace,
name,
endpoint,
service,
hass,
response_type='Response',
payload=None):
"""Assert an API request calls a hass service."""
context = Context()
request = get_new_request(namespace, name, endpoint)
if payload:
request['directive']['payload'] = payload
domain, service_name = service.split('.')
calls = async_mock_service(hass, domain, service_name)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request, context)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert 'event' in msg
assert call.data['entity_id'] == endpoint.replace('#', '.')
assert msg['event']['header']['name'] == response_type
assert call.context == context
return call, msg
async def assert_request_fails(
namespace,
name,
endpoint,
service_not_called,
hass,
payload=None):
"""Assert an API request returns an ErrorResponse."""
request = get_new_request(namespace, name, endpoint)
if payload:
request['directive']['payload'] = payload
domain, service_name = service_not_called.split('.')
call = async_mock_service(hass, domain, service_name)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert not call
assert 'event' in msg
assert msg['event']['header']['name'] == 'ErrorResponse'
return msg
async def assert_power_controller_works(
endpoint,
on_service,
off_service,
hass
):
"""Assert PowerController API requests work."""
await assert_request_calls_service(
'Alexa.PowerController', 'TurnOn', endpoint,
on_service, hass)
await assert_request_calls_service(
'Alexa.PowerController', 'TurnOff', endpoint,
off_service, hass)
async def assert_scene_controller_works(
endpoint,
activate_service,
deactivate_service,
hass):
"""Assert SceneController API requests work."""
_, response = await assert_request_calls_service(
'Alexa.SceneController', 'Activate', endpoint,
activate_service, hass,
response_type='ActivationStarted')
assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION'
assert 'timestamp' in response['event']['payload']
if deactivate_service:
await assert_request_calls_service(
'Alexa.SceneController', 'Deactivate', endpoint,
deactivate_service, hass,
response_type='DeactivationStarted')
cause_type = response['event']['payload']['cause']['type']
assert cause_type == 'VOICE_INTERACTION'
assert 'timestamp' in response['event']['payload']
async def reported_properties(hass, endpoint):
"""Use ReportState to get properties and return them.
The result is a ReportedProperties instance, which has methods to make
assertions about the properties.
"""
request = get_new_request('Alexa', 'ReportState', endpoint)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
return ReportedProperties(msg['context']['properties'])
class ReportedProperties:
"""Class to help assert reported properties."""
def __init__(self, properties):
"""Initialize class."""
self.properties = properties
def assert_equal(self, namespace, name, value):
"""Assert a property is equal to a given value."""
for prop in self.properties:
if prop['namespace'] == namespace and prop['name'] == name:
assert prop['value'] == value
return prop
assert False, 'property %s:%s not in %r' % (
namespace,
name,
self.properties,
)

View File

@ -0,0 +1,67 @@
"""Test Alexa auth endpoints."""
from homeassistant.components.alexa.auth import Auth
from . import TEST_TOKEN_URL
async def run_auth_get_access_token(hass, aioclient_mock, expires_in,
client_id, client_secret,
accept_grant_code, refresh_token):
"""Do auth and request a new token for tests."""
aioclient_mock.post(TEST_TOKEN_URL,
json={'access_token': 'the_access_token',
'refresh_token': refresh_token,
'expires_in': expires_in})
auth = Auth(hass, client_id, client_secret)
await auth.async_do_auth(accept_grant_code)
await auth.async_get_access_token()
async def test_auth_get_access_token_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, -5,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 2
calls = aioclient_mock.mock_calls
auth_call_json = calls[0][2]
token_call_json = calls[1][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret
assert token_call_json["grant_type"] == "refresh_token"
assert token_call_json["refresh_token"] == refresh_token
assert token_call_json["client_id"] == client_id
assert token_call_json["client_secret"] == client_secret
async def test_auth_get_access_token_not_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, 555,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
auth_call_json = call[0][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret

View File

@ -0,0 +1,340 @@
"""Test Alexa capabilities."""
import pytest
from homeassistant.const import (
STATE_LOCKED,
STATE_UNLOCKED,
STATE_UNKNOWN,
)
from homeassistant.components.alexa import smart_home
from tests.common import async_mock_service
from . import (
DEFAULT_CONFIG,
get_new_request,
assert_request_calls_service,
assert_request_fails,
reported_properties,
)
@pytest.mark.parametrize(
"result,adjust", [(25, '-5'), (35, '5'), (0, '-80')])
async def test_api_adjust_brightness(hass, result, adjust):
"""Test api adjust brightness process."""
request = get_new_request(
'Alexa.BrightnessController', 'AdjustBrightness', 'light#test')
# add payload
request['directive']['payload']['brightnessDelta'] = adjust
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'brightness': '77'
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['brightness_pct'] == result
assert msg['header']['name'] == 'Response'
async def test_api_set_color_rgb(hass):
"""Test api set color process."""
request = get_new_request(
'Alexa.ColorController', 'SetColor', 'light#test')
# add payload
request['directive']['payload']['color'] = {
'hue': '120',
'saturation': '0.612',
'brightness': '0.342',
}
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light",
'supported_features': 16,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['rgb_color'] == (33, 87, 33)
assert msg['header']['name'] == 'Response'
async def test_api_set_color_temperature(hass):
"""Test api set color temperature process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'SetColorTemperature',
'light#test')
# add payload
request['directive']['payload']['colorTemperatureInKelvin'] = '7500'
# setup test devices
hass.states.async_set(
'light.test', 'off', {'friendly_name': "Test light"})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['kelvin'] == 7500
assert msg['header']['name'] == 'Response'
@pytest.mark.parametrize("result,initial", [(383, '333'), (500, '500')])
async def test_api_decrease_color_temp(hass, result, initial):
"""Test api decrease color temp process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'DecreaseColorTemperature',
'light#test')
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
'max_mireds': 500,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['color_temp'] == result
assert msg['header']['name'] == 'Response'
@pytest.mark.parametrize("result,initial", [(283, '333'), (142, '142')])
async def test_api_increase_color_temp(hass, result, initial):
"""Test api increase color temp process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'IncreaseColorTemperature',
'light#test')
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
'min_mireds': 142,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['color_temp'] == result
assert msg['header']['name'] == 'Response'
@pytest.mark.parametrize(
"domain,payload,source_list,idx", [
('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1),
('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0),
('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0),
('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None),
]
)
async def test_api_select_input(hass, domain, payload, source_list, idx):
"""Test api set input process."""
hass.states.async_set(
'media_player.test', 'off', {
'friendly_name': "Test media player",
'source': 'unknown',
'source_list': source_list,
})
# test where no source matches
if idx is None:
await assert_request_fails(
'Alexa.InputController', 'SelectInput', 'media_player#test',
'media_player.select_source',
hass,
payload={'input': payload})
return
call, _ = await assert_request_calls_service(
'Alexa.InputController', 'SelectInput', 'media_player#test',
'media_player.select_source',
hass,
payload={'input': payload})
assert call.data['source'] == source_list[idx]
async def test_report_lock_state(hass):
"""Test LockController implements lockState property."""
hass.states.async_set(
'lock.locked', STATE_LOCKED, {})
hass.states.async_set(
'lock.unlocked', STATE_UNLOCKED, {})
hass.states.async_set(
'lock.unknown', STATE_UNKNOWN, {})
properties = await reported_properties(hass, 'lock.locked')
properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED')
properties = await reported_properties(hass, 'lock.unlocked')
properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED')
properties = await reported_properties(hass, 'lock.unknown')
properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED')
async def test_report_dimmable_light_state(hass):
"""Test BrightnessController reports brightness correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'brightness': 128, 'supported_features': 1})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 1})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 0)
async def test_report_colored_light_state(hass):
"""Test ColorController reports color correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'hs_color': (180, 75),
'brightness': 128,
'supported_features': 17})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 17})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.ColorController', 'color', {
'hue': 180,
'saturation': 0.75,
'brightness': 128 / 255.0,
})
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.ColorController', 'color', {
'hue': 0,
'saturation': 0,
'brightness': 0,
})
async def test_report_colored_temp_light_state(hass):
"""Test ColorTemperatureController reports color temp correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'color_temp': 240,
'supported_features': 2})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 2})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.ColorTemperatureController',
'colorTemperatureInKelvin', 4166)
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.ColorTemperatureController',
'colorTemperatureInKelvin', 0)
async def test_report_fan_speed_state(hass):
"""Test PercentageController reports fan speed correctly."""
hass.states.async_set(
'fan.off', 'off', {'friendly_name': "Off fan",
'speed': "off",
'supported_features': 1})
hass.states.async_set(
'fan.low_speed', 'on', {'friendly_name': "Low speed fan",
'speed': "low",
'supported_features': 1})
hass.states.async_set(
'fan.medium_speed', 'on', {'friendly_name': "Medium speed fan",
'speed': "medium",
'supported_features': 1})
hass.states.async_set(
'fan.high_speed', 'on', {'friendly_name': "High speed fan",
'speed': "high",
'supported_features': 1})
properties = await reported_properties(hass, 'fan.off')
properties.assert_equal('Alexa.PercentageController', 'percentage', 0)
properties = await reported_properties(hass, 'fan.low_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 33)
properties = await reported_properties(hass, 'fan.medium_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 66)
properties = await reported_properties(hass, 'fan.high_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 100)
async def test_report_cover_percentage_state(hass):
"""Test PercentageController reports cover percentage correctly."""
hass.states.async_set(
'cover.fully_open', 'open', {'friendly_name': "Fully open cover",
'current_position': 100,
'supported_features': 15})
hass.states.async_set(
'cover.half_open', 'open', {'friendly_name': "Half open cover",
'current_position': 50,
'supported_features': 15})
hass.states.async_set(
'cover.closed', 'closed', {'friendly_name': "Closed cover",
'current_position': 0,
'supported_features': 15})
properties = await reported_properties(hass, 'cover.fully_open')
properties.assert_equal('Alexa.PercentageController', 'percentage', 100)
properties = await reported_properties(hass, 'cover.half_open')
properties.assert_equal('Alexa.PercentageController', 'percentage', 50)
properties = await reported_properties(hass, 'cover.closed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 0)

View File

@ -0,0 +1,19 @@
"""Test Alexa entity representation."""
from homeassistant.components.alexa import smart_home
from . import get_new_request, DEFAULT_CONFIG
async 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 = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
assert not msg['payload']['endpoints']

View File

@ -1,34 +1,27 @@
"""Test for smart home alexa support.""" """Test for smart home alexa support."""
import json
from uuid import uuid4
import pytest import pytest
from homeassistant.core import Context, callback from homeassistant.core import Context, callback
from homeassistant.const import ( from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_LOCKED, from homeassistant.components.alexa import (
STATE_UNLOCKED, STATE_UNKNOWN) config,
from homeassistant.setup import async_setup_component smart_home,
from homeassistant.components import alexa messages,
from homeassistant.components.alexa import smart_home )
from homeassistant.components.alexa.auth import Auth
from homeassistant.helpers import entityfilter from homeassistant.helpers import entityfilter
from tests.common import async_mock_service from tests.common import async_mock_service
from . import (
async def get_access_token(): get_new_request,
"""Return a test access token.""" DEFAULT_CONFIG,
return "thisisnotanacesstoken" assert_request_calls_service,
assert_request_fails,
ReportedProperties,
TEST_URL = "https://api.amazonalexa.com/v3/events" assert_power_controller_works,
TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" assert_scene_controller_works,
reported_properties,
DEFAULT_CONFIG = smart_home.Config( )
endpoint=TEST_URL,
async_get_access_token=get_access_token,
should_expose=lambda entity_id: True)
@pytest.fixture @pytest.fixture
@ -42,39 +35,11 @@ def events(hass):
yield events yield events
def get_new_request(namespace, name, endpoint=None):
"""Generate a new API message."""
raw_msg = {
'directive': {
'header': {
'namespace': namespace,
'name': name,
'messageId': str(uuid4()),
'correlationToken': str(uuid4()),
'payloadVersion': '3',
},
'endpoint': {
'scope': {
'type': 'BearerToken',
'token': str(uuid4()),
},
'endpointId': endpoint,
},
'payload': {},
}
}
if not endpoint:
raw_msg['directive'].pop('endpoint')
return raw_msg
def test_create_api_message_defaults(hass): def test_create_api_message_defaults(hass):
"""Create a API message response of a request with defaults.""" """Create a API message response of a request with defaults."""
request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#xy') request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#xy')
directive_header = request['directive']['header'] directive_header = request['directive']['header']
directive = smart_home._AlexaDirective(request) directive = messages.AlexaDirective(request)
msg = directive.response(payload={'test': 3})._response msg = directive.response(payload={'test': 3})._response
@ -101,7 +66,7 @@ def test_create_api_message_special():
request = get_new_request('Alexa.PowerController', 'TurnOn') request = get_new_request('Alexa.PowerController', 'TurnOn')
directive_header = request['directive']['header'] directive_header = request['directive']['header']
directive_header.pop('correlationToken') directive_header.pop('correlationToken')
directive = smart_home._AlexaDirective(request) directive = messages.AlexaDirective(request)
msg = directive.response('testName', 'testNameSpace')._response msg = directive.response('testName', 'testNameSpace')._response
@ -901,7 +866,7 @@ async def test_thermostat(hass):
payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}} payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}}
) )
assert call.data['temperature'] == 69.0 assert call.data['temperature'] == 69.0
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'targetSetpoint', 'Alexa.ThermostatController', 'targetSetpoint',
{'value': 69.0, 'scale': 'FAHRENHEIT'}) {'value': 69.0, 'scale': 'FAHRENHEIT'})
@ -927,7 +892,7 @@ async def test_thermostat(hass):
assert call.data['temperature'] == 70.0 assert call.data['temperature'] == 70.0
assert call.data['target_temp_low'] == 68.0 assert call.data['target_temp_low'] == 68.0
assert call.data['target_temp_high'] == 86.0 assert call.data['target_temp_high'] == 86.0
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'targetSetpoint', 'Alexa.ThermostatController', 'targetSetpoint',
{'value': 70.0, 'scale': 'FAHRENHEIT'}) {'value': 70.0, 'scale': 'FAHRENHEIT'})
@ -967,7 +932,7 @@ async def test_thermostat(hass):
payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}} payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}}
) )
assert call.data['temperature'] == 52.0 assert call.data['temperature'] == 52.0
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'targetSetpoint', 'Alexa.ThermostatController', 'targetSetpoint',
{'value': 52.0, 'scale': 'FAHRENHEIT'}) {'value': 52.0, 'scale': 'FAHRENHEIT'})
@ -988,7 +953,7 @@ async def test_thermostat(hass):
payload={'thermostatMode': {'value': 'HEAT'}} payload={'thermostatMode': {'value': 'HEAT'}}
) )
assert call.data['operation_mode'] == 'heat' assert call.data['operation_mode'] == 'heat'
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'thermostatMode', 'HEAT') 'Alexa.ThermostatController', 'thermostatMode', 'HEAT')
@ -999,7 +964,7 @@ async def test_thermostat(hass):
payload={'thermostatMode': {'value': 'COOL'}} payload={'thermostatMode': {'value': 'COOL'}}
) )
assert call.data['operation_mode'] == 'cool' assert call.data['operation_mode'] == 'cool'
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'thermostatMode', 'COOL') 'Alexa.ThermostatController', 'thermostatMode', 'COOL')
@ -1011,7 +976,7 @@ async def test_thermostat(hass):
payload={'thermostatMode': 'HEAT'} payload={'thermostatMode': 'HEAT'}
) )
assert call.data['operation_mode'] == 'heat' assert call.data['operation_mode'] == 'heat'
properties = _ReportedProperties(msg['context']['properties']) properties = ReportedProperties(msg['context']['properties'])
properties.assert_equal( properties.assert_equal(
'Alexa.ThermostatController', 'thermostatMode', 'HEAT') 'Alexa.ThermostatController', 'thermostatMode', 'HEAT')
@ -1047,7 +1012,7 @@ async def test_exclude_filters(hass):
hass.states.async_set( hass.states.async_set(
'cover.deny', 'off', {'friendly_name': "Blocked cover"}) 'cover.deny', 'off', {'friendly_name': "Blocked cover"})
config = smart_home.Config( alexa_config = config.Config(
endpoint=None, endpoint=None,
async_get_access_token=None, async_get_access_token=None,
should_expose=entityfilter.generate_filter( should_expose=entityfilter.generate_filter(
@ -1057,7 +1022,7 @@ async def test_exclude_filters(hass):
exclude_entities=['cover.deny'], exclude_entities=['cover.deny'],
)) ))
msg = await smart_home.async_handle_message(hass, config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
msg = msg['event'] msg = msg['event']
@ -1082,7 +1047,7 @@ async def test_include_filters(hass):
hass.states.async_set( hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"}) 'group.allow', 'off', {'friendly_name': "Allowed group"})
config = smart_home.Config( alexa_config = config.Config(
endpoint=None, endpoint=None,
async_get_access_token=None, async_get_access_token=None,
should_expose=entityfilter.generate_filter( should_expose=entityfilter.generate_filter(
@ -1092,7 +1057,7 @@ async def test_include_filters(hass):
exclude_entities=[], exclude_entities=[],
)) ))
msg = await smart_home.async_handle_message(hass, config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
msg = msg['event'] msg = msg['event']
@ -1111,7 +1076,7 @@ async def test_never_exposed_entities(hass):
hass.states.async_set( hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"}) 'group.allow', 'off', {'friendly_name': "Allowed group"})
config = smart_home.Config( alexa_config = config.Config(
endpoint=None, endpoint=None,
async_get_access_token=None, async_get_access_token=None,
should_expose=entityfilter.generate_filter( should_expose=entityfilter.generate_filter(
@ -1121,7 +1086,7 @@ async def test_never_exposed_entities(hass):
exclude_entities=[], exclude_entities=[],
)) ))
msg = await smart_home.async_handle_message(hass, config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
msg = msg['event'] msg = msg['event']
@ -1162,267 +1127,20 @@ async def test_api_function_not_implemented(hass):
assert msg['payload']['type'] == 'INTERNAL_ERROR' assert msg['payload']['type'] == 'INTERNAL_ERROR'
async def assert_request_fails(
namespace,
name,
endpoint,
service_not_called,
hass,
payload=None):
"""Assert an API request returns an ErrorResponse."""
request = get_new_request(namespace, name, endpoint)
if payload:
request['directive']['payload'] = payload
domain, service_name = service_not_called.split('.')
call = async_mock_service(hass, domain, service_name)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert not call
assert 'event' in msg
assert msg['event']['header']['name'] == 'ErrorResponse'
return msg
async def assert_request_calls_service(
namespace,
name,
endpoint,
service,
hass,
response_type='Response',
payload=None):
"""Assert an API request calls a hass service."""
context = Context()
request = get_new_request(namespace, name, endpoint)
if payload:
request['directive']['payload'] = payload
domain, service_name = service.split('.')
calls = async_mock_service(hass, domain, service_name)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request, context)
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert 'event' in msg
assert call.data['entity_id'] == endpoint.replace('#', '.')
assert msg['event']['header']['name'] == response_type
assert call.context == context
return call, msg
async def assert_power_controller_works(
endpoint,
on_service,
off_service,
hass
):
"""Assert PowerController API requests work."""
await assert_request_calls_service(
'Alexa.PowerController', 'TurnOn', endpoint,
on_service, hass)
await assert_request_calls_service(
'Alexa.PowerController', 'TurnOff', endpoint,
off_service, hass)
async def assert_scene_controller_works(
endpoint,
activate_service,
deactivate_service,
hass):
"""Assert SceneController API requests work."""
_, response = await assert_request_calls_service(
'Alexa.SceneController', 'Activate', endpoint,
activate_service, hass,
response_type='ActivationStarted')
assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION'
assert 'timestamp' in response['event']['payload']
if deactivate_service:
await assert_request_calls_service(
'Alexa.SceneController', 'Deactivate', endpoint,
deactivate_service, hass,
response_type='DeactivationStarted')
cause_type = response['event']['payload']['cause']['type']
assert cause_type == 'VOICE_INTERACTION'
assert 'timestamp' in response['event']['payload']
@pytest.mark.parametrize(
"result,adjust", [(25, '-5'), (35, '5'), (0, '-80')])
async def test_api_adjust_brightness(hass, result, adjust):
"""Test api adjust brightness process."""
request = get_new_request(
'Alexa.BrightnessController', 'AdjustBrightness', 'light#test')
# add payload
request['directive']['payload']['brightnessDelta'] = adjust
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'brightness': '77'
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['brightness_pct'] == result
assert msg['header']['name'] == 'Response'
async def test_api_set_color_rgb(hass):
"""Test api set color process."""
request = get_new_request(
'Alexa.ColorController', 'SetColor', 'light#test')
# add payload
request['directive']['payload']['color'] = {
'hue': '120',
'saturation': '0.612',
'brightness': '0.342',
}
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light",
'supported_features': 16,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['rgb_color'] == (33, 87, 33)
assert msg['header']['name'] == 'Response'
async def test_api_set_color_temperature(hass):
"""Test api set color temperature process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'SetColorTemperature',
'light#test')
# add payload
request['directive']['payload']['colorTemperatureInKelvin'] = '7500'
# setup test devices
hass.states.async_set(
'light.test', 'off', {'friendly_name': "Test light"})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['kelvin'] == 7500
assert msg['header']['name'] == 'Response'
@pytest.mark.parametrize("result,initial", [(383, '333'), (500, '500')])
async def test_api_decrease_color_temp(hass, result, initial):
"""Test api decrease color temp process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'DecreaseColorTemperature',
'light#test')
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
'max_mireds': 500,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['color_temp'] == result
assert msg['header']['name'] == 'Response'
@pytest.mark.parametrize("result,initial", [(283, '333'), (142, '142')])
async def test_api_increase_color_temp(hass, result, initial):
"""Test api increase color temp process."""
request = get_new_request(
'Alexa.ColorTemperatureController', 'IncreaseColorTemperature',
'light#test')
# setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
'min_mireds': 142,
})
call_light = async_mock_service(hass, 'light', 'turn_on')
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['color_temp'] == result
assert msg['header']['name'] == 'Response'
async def test_api_accept_grant(hass): async def test_api_accept_grant(hass):
"""Test api AcceptGrant process.""" """Test api AcceptGrant process."""
request = get_new_request("Alexa.Authorization", "AcceptGrant") request = get_new_request("Alexa.Authorization", "AcceptGrant")
# add payload # add payload
request['directive']['payload'] = { request['directive']['payload'] = {
'grant': { 'grant': {
'type': 'OAuth2.AuthorizationCode', 'type': 'OAuth2.AuthorizationCode',
'code': 'VGhpcyBpcyBhbiBhdXRob3JpemF0aW9uIGNvZGUuIDotKQ==' 'code': 'VGhpcyBpcyBhbiBhdXRob3JpemF0aW9uIGNvZGUuIDotKQ=='
}, },
'grantee': { 'grantee': {
'type': 'BearerToken', 'type': 'BearerToken',
'token': 'access-token-from-skill' 'token': 'access-token-from-skill'
} }
} }
# setup test devices # setup test devices
@ -1436,174 +1154,6 @@ async def test_api_accept_grant(hass):
assert msg['header']['name'] == 'AcceptGrant.Response' assert msg['header']['name'] == 'AcceptGrant.Response'
async def test_report_lock_state(hass):
"""Test LockController implements lockState property."""
hass.states.async_set(
'lock.locked', STATE_LOCKED, {})
hass.states.async_set(
'lock.unlocked', STATE_UNLOCKED, {})
hass.states.async_set(
'lock.unknown', STATE_UNKNOWN, {})
properties = await reported_properties(hass, 'lock.locked')
properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED')
properties = await reported_properties(hass, 'lock.unlocked')
properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED')
properties = await reported_properties(hass, 'lock.unknown')
properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED')
async def test_report_dimmable_light_state(hass):
"""Test BrightnessController reports brightness correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'brightness': 128, 'supported_features': 1})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 1})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 0)
async def test_report_colored_light_state(hass):
"""Test ColorController reports color correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'hs_color': (180, 75),
'brightness': 128,
'supported_features': 17})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 17})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.ColorController', 'color', {
'hue': 180,
'saturation': 0.75,
'brightness': 128 / 255.0,
})
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.ColorController', 'color', {
'hue': 0,
'saturation': 0,
'brightness': 0,
})
async def test_report_colored_temp_light_state(hass):
"""Test ColorTemperatureController reports color temp correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'color_temp': 240,
'supported_features': 2})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 2})
properties = await reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.ColorTemperatureController',
'colorTemperatureInKelvin', 4166)
properties = await reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.ColorTemperatureController',
'colorTemperatureInKelvin', 0)
async def test_report_fan_speed_state(hass):
"""Test PercentageController reports fan speed correctly."""
hass.states.async_set(
'fan.off', 'off', {'friendly_name': "Off fan",
'speed': "off",
'supported_features': 1})
hass.states.async_set(
'fan.low_speed', 'on', {'friendly_name': "Low speed fan",
'speed': "low",
'supported_features': 1})
hass.states.async_set(
'fan.medium_speed', 'on', {'friendly_name': "Medium speed fan",
'speed': "medium",
'supported_features': 1})
hass.states.async_set(
'fan.high_speed', 'on', {'friendly_name': "High speed fan",
'speed': "high",
'supported_features': 1})
properties = await reported_properties(hass, 'fan.off')
properties.assert_equal('Alexa.PercentageController', 'percentage', 0)
properties = await reported_properties(hass, 'fan.low_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 33)
properties = await reported_properties(hass, 'fan.medium_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 66)
properties = await reported_properties(hass, 'fan.high_speed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 100)
async def test_report_cover_percentage_state(hass):
"""Test PercentageController reports cover percentage correctly."""
hass.states.async_set(
'cover.fully_open', 'open', {'friendly_name': "Fully open cover",
'current_position': 100,
'supported_features': 15})
hass.states.async_set(
'cover.half_open', 'open', {'friendly_name': "Half open cover",
'current_position': 50,
'supported_features': 15})
hass.states.async_set(
'cover.closed', 'closed', {'friendly_name': "Closed cover",
'current_position': 0,
'supported_features': 15})
properties = await reported_properties(hass, 'cover.fully_open')
properties.assert_equal('Alexa.PercentageController', 'percentage', 100)
properties = await reported_properties(hass, 'cover.half_open')
properties.assert_equal('Alexa.PercentageController', 'percentage', 50)
properties = await reported_properties(hass, 'cover.closed')
properties.assert_equal('Alexa.PercentageController', 'percentage', 0)
async def reported_properties(hass, endpoint):
"""Use ReportState to get properties and return them.
The result is a _ReportedProperties instance, which has methods to make
assertions about the properties.
"""
request = get_new_request('Alexa', 'ReportState', endpoint)
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
return _ReportedProperties(msg['context']['properties'])
class _ReportedProperties:
def __init__(self, properties):
self.properties = properties
def assert_equal(self, namespace, name, value):
"""Assert a property is equal to a given value."""
for prop in self.properties:
if prop['namespace'] == namespace and prop['name'] == name:
assert prop['value'] == value
return prop
assert False, 'property %s:%s not in %r' % (
namespace,
name,
self.properties,
)
async def test_entity_config(hass): async def test_entity_config(hass):
"""Test that we can configure things via entity config.""" """Test that we can configure things via entity config."""
request = get_new_request('Alexa.Discovery', 'Discover') request = get_new_request('Alexa.Discovery', 'Discover')
@ -1611,7 +1161,7 @@ async def test_entity_config(hass):
hass.states.async_set( hass.states.async_set(
'light.test_1', 'on', {'friendly_name': "Test light 1"}) 'light.test_1', 'on', {'friendly_name': "Test light 1"})
config = smart_home.Config( alexa_config = config.Config(
endpoint=None, endpoint=None,
async_get_access_token=None, async_get_access_token=None,
should_expose=lambda entity_id: True, should_expose=lambda entity_id: True,
@ -1625,7 +1175,7 @@ async def test_entity_config(hass):
) )
msg = await smart_home.async_handle_message( msg = await smart_home.async_handle_message(
hass, config, request) hass, alexa_config, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -1644,95 +1194,6 @@ async def test_entity_config(hass):
) )
async 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 = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
assert not msg['payload']['endpoints']
async def do_http_discovery(config, hass, hass_client):
"""Submit a request to the Smart Home HTTP API."""
await async_setup_component(hass, alexa.DOMAIN, config)
http_client = await hass_client()
request = get_new_request('Alexa.Discovery', 'Discover')
response = await http_client.post(
smart_home.SMART_HOME_HTTP_ENDPOINT,
data=json.dumps(request),
headers={'content-type': 'application/json'})
return response
async def test_http_api(hass, hass_client):
"""With `smart_home:` HTTP API is exposed."""
config = {
'alexa': {
'smart_home': None
}
}
response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json()
# Here we're testing just the HTTP view glue -- details of discovery are
# covered in other tests.
assert response_data['event']['header']['name'] == 'Discover.Response'
async def test_http_api_disabled(hass, hass_client):
"""Without `smart_home:`, the HTTP API is disabled."""
config = {
'alexa': {}
}
response = await do_http_discovery(config, hass, hass_client)
assert response.status == 404
@pytest.mark.parametrize(
"domain,payload,source_list,idx", [
('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1),
('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0),
('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0),
('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None),
]
)
async def test_api_select_input(hass, domain, payload, source_list, idx):
"""Test api set input process."""
hass.states.async_set(
'media_player.test', 'off', {
'friendly_name': "Test media player",
'source': 'unknown',
'source_list': source_list,
})
# test where no source matches
if idx is None:
await assert_request_fails(
'Alexa.InputController', 'SelectInput', 'media_player#test',
'media_player.select_source',
hass,
payload={'input': payload})
return
call, _ = await assert_request_calls_service(
'Alexa.InputController', 'SelectInput', 'media_player#test',
'media_player.select_source',
hass,
payload={'input': payload})
assert call.data['source'] == source_list[idx]
async def test_logging_request(hass, events): async def test_logging_request(hass, events):
"""Test that we log requests.""" """Test that we log requests."""
context = Context() context = Context()
@ -1834,104 +1295,3 @@ async def test_endpoint_bad_health(hass):
properties = await reported_properties(hass, 'binary_sensor#test_contact') properties = await reported_properties(hass, 'binary_sensor#test_contact')
properties.assert_equal('Alexa.EndpointHealth', 'connectivity', properties.assert_equal('Alexa.EndpointHealth', 'connectivity',
{'value': 'UNREACHABLE'}) {'value': 'UNREACHABLE'})
async def test_report_state(hass, aioclient_mock):
"""Test proactive state reports."""
aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'})
hass.states.async_set(
'binary_sensor.test_contact',
'on',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
await smart_home.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
hass.states.async_set(
'binary_sensor.test_contact',
'off',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
# To trigger event listener
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
call_json = call[0][2]
assert call_json["event"]["payload"]["change"]["properties"][0][
"value"] == "NOT_DETECTED"
assert call_json["event"]["endpoint"][
"endpointId"] == "binary_sensor#test_contact"
async def run_auth_get_access_token(hass, aioclient_mock, expires_in,
client_id, client_secret,
accept_grant_code, refresh_token):
"""Do auth and request a new token for tests."""
aioclient_mock.post(TEST_TOKEN_URL,
json={'access_token': 'the_access_token',
'refresh_token': refresh_token,
'expires_in': expires_in})
auth = Auth(hass, client_id, client_secret)
await auth.async_do_auth(accept_grant_code)
await auth.async_get_access_token()
async def test_auth_get_access_token_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, -5,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 2
calls = aioclient_mock.mock_calls
auth_call_json = calls[0][2]
token_call_json = calls[1][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret
assert token_call_json["grant_type"] == "refresh_token"
assert token_call_json["refresh_token"] == refresh_token
assert token_call_json["client_id"] == client_id
assert token_call_json["client_secret"] == client_secret
async def test_auth_get_access_token_not_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, 555,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
auth_call_json = call[0][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret

View File

@ -0,0 +1,46 @@
"""Test Smart Home HTTP endpoints."""
import json
from homeassistant.setup import async_setup_component
from homeassistant.components.alexa import DOMAIN, smart_home_http
from . import get_new_request
async def do_http_discovery(config, hass, hass_client):
"""Submit a request to the Smart Home HTTP API."""
await async_setup_component(hass, DOMAIN, config)
http_client = await hass_client()
request = get_new_request('Alexa.Discovery', 'Discover')
response = await http_client.post(
smart_home_http.SMART_HOME_HTTP_ENDPOINT,
data=json.dumps(request),
headers={'content-type': 'application/json'})
return response
async def test_http_api(hass, hass_client):
"""With `smart_home:` HTTP API is exposed."""
config = {
'alexa': {
'smart_home': None
}
}
response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json()
# Here we're testing just the HTTP view glue -- details of discovery are
# covered in other tests.
assert response_data['event']['header']['name'] == 'Discover.Response'
async def test_http_api_disabled(hass, hass_client):
"""Without `smart_home:`, the HTTP API is disabled."""
config = {
'alexa': {}
}
response = await do_http_discovery(config, hass, hass_client)
assert response.status == 404

View File

@ -0,0 +1,40 @@
"""Test report state."""
from homeassistant.components.alexa import state_report
from . import TEST_URL, DEFAULT_CONFIG
async def test_report_state(hass, aioclient_mock):
"""Test proactive state reports."""
aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'})
hass.states.async_set(
'binary_sensor.test_contact',
'on',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
hass.states.async_set(
'binary_sensor.test_contact',
'off',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
# To trigger event listener
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
call_json = call[0][2]
assert call_json["event"]["payload"]["change"]["properties"][0]["value"] \
== "NOT_DETECTED"
assert call_json["event"]["endpoint"]["endpointId"] \
== "binary_sensor#test_contact"

View File

@ -343,7 +343,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
with patch.dict( with patch.dict(
'homeassistant.components.google_assistant.const.' 'homeassistant.components.google_assistant.const.'
'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True 'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True
), patch.dict('homeassistant.components.alexa.smart_home.ENTITY_ADAPTERS', ), patch.dict('homeassistant.components.alexa.entities.ENTITY_ADAPTERS',
{'switch': None}, clear=True): {'switch': None}, clear=True):
await client.send_json({ await client.send_json({
'id': 5, 'id': 5,