From 7d6ef4445ee1db07ff9903f7c809d93bdf16c377 Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Mon, 29 Jan 2018 01:00:34 +0000 Subject: [PATCH] Report states (#11973) * Refactor Alexa Smart Home API Having an object per interface will make it easier to support properties. Ideally, properties are reported in context in all responses. However current implementation reports them only in response to a ReportState request. This seems to work sufficiently. As long as the device is opened in the Alexa app, Amazon will poll the device state every few seconds with a ReportState request. * Report properties for some Alexa interfaces Fixes (mostly) #11874. Other interfaces will need properties implemented as well. Implementing properties for just PowerController seems sufficient to eliminate the "There was a problem." error for any device that supports it, even if other interfaces are supported. Of course the additional properties will be reported incorrectly in the Alexa app. Includes a minor bugfix: `reportable` was previously placed incorrectly in the responses, so Amazon was ignoring it. --- homeassistant/components/alexa/smart_home.py | 47 ++++++++- tests/components/alexa/test_smart_home.py | 101 ++++++++++++++++++- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index e09ee751e43..6721c0038e8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -17,7 +17,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -245,9 +245,8 @@ class _AlexaInterface(object): 'properties': { 'supported': self.properties_supported(), 'proactivelyReported': self.properties_proactively_reported(), + 'retrievable': self.properties_retrievable(), }, - # XXX this is incorrect, but the tests assert it - 'retrievable': self.properties_retrievable(), } # pylint: disable=assignment-from-none @@ -271,11 +270,41 @@ class _AlexaPowerController(_AlexaInterface): def name(self): return 'Alexa.PowerController' + def properties_supported(self): + return [{'name': 'powerState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'powerState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_ON: + return 'ON' + return 'OFF' + class _AlexaLockController(_AlexaInterface): def name(self): return 'Alexa.LockController' + def properties_supported(self): + return [{'name': 'lockState'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'lockState': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_LOCKED: + return 'LOCKED' + elif self.entity.state == STATE_UNLOCKED: + return 'UNLOCKED' + return 'JAMMED' + class _AlexaSceneController(_AlexaInterface): def __init__(self, entity, supports_deactivation): @@ -290,6 +319,18 @@ class _AlexaBrightnessController(_AlexaInterface): def name(self): return 'Alexa.BrightnessController' + def properties_supported(self): + return [{'name': 'brightness'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'brightness': + raise _UnsupportedProperty(name) + + return round(self.entity.attributes['brightness'] / 255.0 * 100) + class _AlexaColorController(_AlexaInterface): def name(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1b2e98d6558..7795bc85f1a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,9 +5,11 @@ from uuid import uuid4 import pytest -from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, + STATE_UNKNOWN, STATE_ON, STATE_OFF) from homeassistant.setup import async_setup_component -from homeassistant.components import alexa +from homeassistant.components import alexa, light from homeassistant.components.alexa import smart_home from homeassistant.helpers import entityfilter @@ -379,8 +381,8 @@ def test_discovery_request(hass): assert len(appliance['capabilities']) == 1 capability = appliance['capabilities'][0] assert capability['interface'] == 'Alexa.TemperatureSensor' - assert capability['retrievable'] is True properties = capability['properties'] + assert properties['retrievable'] is True assert {'name': 'temperature'} in properties['supported'] continue @@ -1248,6 +1250,99 @@ def test_api_report_temperature(hass): assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'} +@asyncio.coroutine +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, {}) + + request = get_new_request('Alexa', 'ReportState', 'lock#locked') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.LockController' + assert prop['name'] == 'lockState' + assert prop['value'] == 'LOCKED' + + request = get_new_request('Alexa', 'ReportState', 'lock#unlocked') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + prop = properties[0] + assert prop['value'] == 'UNLOCKED' + + request = get_new_request('Alexa', 'ReportState', 'lock#unknown') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + prop = properties[0] + assert prop['value'] == 'JAMMED' + + +@asyncio.coroutine +def test_report_power_state(hass): + """Test PowerController implements powerState property.""" + hass.states.async_set( + 'switch.on', STATE_ON, {}) + hass.states.async_set( + 'switch.off', STATE_OFF, {}) + + request = get_new_request('Alexa', 'ReportState', 'switch#on') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + properties = msg['context']['properties'] + assert len(properties) == 1 + prop = properties[0] + assert prop['namespace'] == 'Alexa.PowerController' + assert prop['name'] == 'powerState' + assert prop['value'] == 'ON' + + request = get_new_request('Alexa', 'ReportState', 'switch#off') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + +@asyncio.coroutine +def test_report_brightness(hass): + """Test BrightnessController implements brightness property.""" + hass.states.async_set( + 'light.test', STATE_ON, { + 'brightness': 128, + 'supported_features': light.SUPPORT_BRIGHTNESS, + } + ) + + request = get_new_request('Alexa', 'ReportState', 'light.test') + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() + + for prop in msg['context']['properties']: + if ( + prop['namespace'] == 'Alexa.BrightnessController' + and prop['name'] == 'brightness' + ): + assert prop['value'] == 50 + break + else: + assert False, 'no brightness property present' + + @asyncio.coroutine def test_entity_config(hass): """Test that we can configure things via entity config."""