Implement Alexa temperature sensors (#11930)
parent
abde8c40c9
commit
7bbef68b2a
|
@ -7,7 +7,7 @@ from uuid import uuid4
|
|||
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch, http)
|
||||
media_player, scene, script, switch, http, sensor)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
@ -16,7 +16,8 @@ from homeassistant.const import (
|
|||
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_SET)
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
CONF_UNIT_OF_MEASUREMENT)
|
||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -24,9 +25,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||
API_DIRECTIVE = 'directive'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
API_EVENT = 'event'
|
||||
API_CONTEXT = 'context'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
|
||||
API_TEMP_UNITS = {
|
||||
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
||||
TEMP_CELSIUS: 'CELSIUS',
|
||||
}
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
|
@ -94,6 +101,8 @@ class _DisplayCategory(object):
|
|||
def _capability(interface,
|
||||
version=3,
|
||||
supports_deactivation=None,
|
||||
retrievable=None,
|
||||
properties_supported=None,
|
||||
cap_type='AlexaInterface'):
|
||||
"""Return a Smart Home API capability object.
|
||||
|
||||
|
@ -102,9 +111,7 @@ def _capability(interface,
|
|||
There are some additional fields allowed but not implemented here since
|
||||
we've no use case for them yet:
|
||||
|
||||
- properties.supported
|
||||
- proactively_reported
|
||||
- retrievable
|
||||
|
||||
`supports_deactivation` applies only to scenes.
|
||||
"""
|
||||
|
@ -117,6 +124,12 @@ def _capability(interface,
|
|||
if supports_deactivation is not None:
|
||||
result['supportsDeactivation'] = supports_deactivation
|
||||
|
||||
if retrievable is not None:
|
||||
result['retrievable'] = retrievable
|
||||
|
||||
if properties_supported is not None:
|
||||
result['properties'] = {'supported': properties_supported}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
@ -144,6 +157,8 @@ class _EntityCapabilities(object):
|
|||
def capabilities(self):
|
||||
"""Return a list of supported capabilities.
|
||||
|
||||
If the returned list is empty, the entity will not be discovered.
|
||||
|
||||
You might find _capability() useful.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
@ -269,6 +284,28 @@ class _GroupCapabilities(_EntityCapabilities):
|
|||
supports_deactivation=True)]
|
||||
|
||||
|
||||
class _SensorCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
# although there are other kinds of sensors, all but temperature
|
||||
# sensors are currently ignored.
|
||||
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = []
|
||||
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
capabilities.append(_capability(
|
||||
'Alexa.TemperatureSensor',
|
||||
retrievable=True,
|
||||
properties_supported=[{'name': 'temperature'}]))
|
||||
|
||||
return capabilities
|
||||
|
||||
|
||||
class _UnknownEntityDomainError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -296,6 +333,7 @@ _CAPABILITIES_FOR_DOMAIN = {
|
|||
scene.DOMAIN: _SceneCapabilities,
|
||||
script.DOMAIN: _ScriptCapabilities,
|
||||
switch.DOMAIN: _SwitchCapabilities,
|
||||
sensor.DOMAIN: _SensorCapabilities,
|
||||
}
|
||||
|
||||
|
||||
|
@ -407,7 +445,11 @@ def async_handle_message(hass, config, message):
|
|||
return (yield from funct_ref(hass, config, message))
|
||||
|
||||
|
||||
def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||
def api_message(request,
|
||||
name='Response',
|
||||
namespace='Alexa',
|
||||
payload=None,
|
||||
context=None):
|
||||
"""Create a API formatted response message.
|
||||
|
||||
Async friendly.
|
||||
|
@ -435,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None):
|
|||
if API_ENDPOINT in request:
|
||||
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
||||
|
||||
if context is not None:
|
||||
response[API_CONTEXT] = context
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
@ -490,7 +535,12 @@ def async_api_discovery(hass, config, request):
|
|||
'manufacturerName': 'Home Assistant',
|
||||
}
|
||||
|
||||
endpoint['capabilities'] = entity_capabilities.capabilities()
|
||||
alexa_capabilities = entity_capabilities.capabilities()
|
||||
if not alexa_capabilities:
|
||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||
entity.entity_id)
|
||||
continue
|
||||
endpoint['capabilities'] = alexa_capabilities
|
||||
discovery_endpoints.append(endpoint)
|
||||
|
||||
return api_message(
|
||||
|
@ -976,3 +1026,25 @@ def async_api_previous(hass, config, request, entity):
|
|||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_reportstate(hass, config, request, entity):
|
||||
"""Process a ReportState request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp_property = {
|
||||
'namespace': 'Alexa.TemperatureSensor',
|
||||
'name': 'temperature',
|
||||
'value': {
|
||||
'value': float(entity.state),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
|
||||
return api_message(
|
||||
request,
|
||||
name='StateReport',
|
||||
context={'properties': [temp_property]}
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import alexa
|
||||
from homeassistant.components.alexa import smart_home
|
||||
|
@ -166,13 +167,27 @@ def test_discovery_request(hass):
|
|||
'position': 85
|
||||
})
|
||||
|
||||
hass.states.async_set(
|
||||
'sensor.test_temp', '59', {
|
||||
'friendly_name': "Test Temp Sensor",
|
||||
'unit_of_measurement': TEMP_FAHRENHEIT,
|
||||
})
|
||||
|
||||
# This sensor measures a quantity not applicable to Alexa, and should not
|
||||
# be discovered.
|
||||
hass.states.async_set(
|
||||
'sensor.test_sickness', '0.1', {
|
||||
'friendly_name': "Test Space Sickness Sensor",
|
||||
'unit_of_measurement': 'garn',
|
||||
})
|
||||
|
||||
msg = yield from smart_home.async_handle_message(
|
||||
hass, DEFAULT_CONFIG, request)
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert len(msg['payload']['endpoints']) == 16
|
||||
assert len(msg['payload']['endpoints']) == 17
|
||||
assert msg['header']['name'] == 'Discover.Response'
|
||||
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
||||
|
||||
|
@ -334,6 +349,17 @@ def test_discovery_request(hass):
|
|||
assert 'Alexa.PowerController' in caps
|
||||
continue
|
||||
|
||||
if appliance['endpointId'] == 'sensor#test_temp':
|
||||
assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR'
|
||||
assert appliance['friendlyName'] == 'Test Temp Sensor'
|
||||
assert len(appliance['capabilities']) == 1
|
||||
capability = appliance['capabilities'][0]
|
||||
assert capability['interface'] == 'Alexa.TemperatureSensor'
|
||||
assert capability['retrievable'] is True
|
||||
properties = capability['properties']
|
||||
assert {'name': 'temperature'} in properties['supported']
|
||||
continue
|
||||
|
||||
raise AssertionError("Unknown appliance!")
|
||||
|
||||
|
||||
|
@ -1170,6 +1196,34 @@ def test_api_mute(hass, domain):
|
|||
assert msg['header']['name'] == 'Response'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_report_temperature(hass):
|
||||
"""Test API ReportState response for a temperature sensor."""
|
||||
request = get_new_request('Alexa', 'ReportState', 'sensor#test')
|
||||
|
||||
# setup test devices
|
||||
hass.states.async_set(
|
||||
'sensor.test', '42', {
|
||||
'friendly_name': 'test sensor',
|
||||
CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
|
||||
})
|
||||
|
||||
msg = yield from smart_home.async_handle_message(
|
||||
hass, DEFAULT_CONFIG, request)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
header = msg['event']['header']
|
||||
assert header['namespace'] == 'Alexa'
|
||||
assert header['name'] == 'StateReport'
|
||||
|
||||
properties = msg['context']['properties']
|
||||
assert len(properties) == 1
|
||||
prop = properties[0]
|
||||
assert prop['namespace'] == 'Alexa.TemperatureSensor'
|
||||
assert prop['name'] == 'temperature'
|
||||
assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_entity_config(hass):
|
||||
"""Test that we can configure things via entity config."""
|
||||
|
|
Loading…
Reference in New Issue