mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Clean up Alexa smart home code (#24514)
* Clean up Alexa smart home code * lint * Lint * Lint
This commit is contained in:
parent
416ff10ba9
commit
7e2278f1cc
@ -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
|
||||||
|
@ -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,
|
||||||
|
597
homeassistant/components/alexa/capabilities.py
Normal file
597
homeassistant/components/alexa/capabilities.py
Normal 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],
|
||||||
|
}
|
13
homeassistant/components/alexa/config.py
Normal file
13
homeassistant/components/alexa/config.py
Normal 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 {}
|
@ -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'
|
||||||
|
445
homeassistant/components/alexa/entities.py
Normal file
445
homeassistant/components/alexa/entities.py
Normal 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
|
87
homeassistant/components/alexa/errors.py
Normal file
87
homeassistant/components/alexa/errors.py
Normal 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'
|
728
homeassistant/components/alexa/handlers.py
Normal file
728
homeassistant/components/alexa/handlers.py
Normal 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')
|
200
homeassistant/components/alexa/messages.py
Normal file
200
homeassistant/components/alexa/messages.py
Normal 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
81
homeassistant/components/alexa/smart_home_http.py
Normal file
81
homeassistant/components/alexa/smart_home_http.py
Normal 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)
|
109
homeassistant/components/alexa/state_report.py
Normal file
109
homeassistant/components/alexa/state_report.py
Normal 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"])
|
@ -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({
|
||||||
|
@ -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],
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
67
tests/components/alexa/test_auth.py
Normal file
67
tests/components/alexa/test_auth.py
Normal 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
|
340
tests/components/alexa/test_capabilities.py
Normal file
340
tests/components/alexa/test_capabilities.py
Normal 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)
|
19
tests/components/alexa/test_entities.py
Normal file
19
tests/components/alexa/test_entities.py
Normal 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']
|
@ -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
|
|
||||||
|
46
tests/components/alexa/test_smart_home_http.py
Normal file
46
tests/components/alexa/test_smart_home_http.py
Normal 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
|
40
tests/components/alexa/test_state_report.py
Normal file
40
tests/components/alexa/test_state_report.py
Normal 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"
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user