Implement Alexa temperature sensors (#11930)
parent
2d8ef36a6c
commit
ffcc41d6ef
|
@ -7,7 +7,7 @@ from uuid import uuid4
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
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.core as ha
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
from homeassistant.util.decorator import Registry
|
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_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
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
|
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -24,9 +25,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
API_DIRECTIVE = 'directive'
|
API_DIRECTIVE = 'directive'
|
||||||
API_ENDPOINT = 'endpoint'
|
API_ENDPOINT = 'endpoint'
|
||||||
API_EVENT = 'event'
|
API_EVENT = 'event'
|
||||||
|
API_CONTEXT = 'context'
|
||||||
API_HEADER = 'header'
|
API_HEADER = 'header'
|
||||||
API_PAYLOAD = 'payload'
|
API_PAYLOAD = 'payload'
|
||||||
|
|
||||||
|
API_TEMP_UNITS = {
|
||||||
|
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
||||||
|
TEMP_CELSIUS: 'CELSIUS',
|
||||||
|
}
|
||||||
|
|
||||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||||
|
|
||||||
CONF_DESCRIPTION = 'description'
|
CONF_DESCRIPTION = 'description'
|
||||||
|
@ -94,6 +101,8 @@ class _DisplayCategory(object):
|
||||||
def _capability(interface,
|
def _capability(interface,
|
||||||
version=3,
|
version=3,
|
||||||
supports_deactivation=None,
|
supports_deactivation=None,
|
||||||
|
retrievable=None,
|
||||||
|
properties_supported=None,
|
||||||
cap_type='AlexaInterface'):
|
cap_type='AlexaInterface'):
|
||||||
"""Return a Smart Home API capability object.
|
"""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
|
There are some additional fields allowed but not implemented here since
|
||||||
we've no use case for them yet:
|
we've no use case for them yet:
|
||||||
|
|
||||||
- properties.supported
|
|
||||||
- proactively_reported
|
- proactively_reported
|
||||||
- retrievable
|
|
||||||
|
|
||||||
`supports_deactivation` applies only to scenes.
|
`supports_deactivation` applies only to scenes.
|
||||||
"""
|
"""
|
||||||
|
@ -117,6 +124,12 @@ def _capability(interface,
|
||||||
if supports_deactivation is not None:
|
if supports_deactivation is not None:
|
||||||
result['supportsDeactivation'] = supports_deactivation
|
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
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,6 +157,8 @@ class _EntityCapabilities(object):
|
||||||
def capabilities(self):
|
def capabilities(self):
|
||||||
"""Return a list of supported capabilities.
|
"""Return a list of supported capabilities.
|
||||||
|
|
||||||
|
If the returned list is empty, the entity will not be discovered.
|
||||||
|
|
||||||
You might find _capability() useful.
|
You might find _capability() useful.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -269,6 +284,28 @@ class _GroupCapabilities(_EntityCapabilities):
|
||||||
supports_deactivation=True)]
|
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):
|
class _UnknownEntityDomainError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -296,6 +333,7 @@ _CAPABILITIES_FOR_DOMAIN = {
|
||||||
scene.DOMAIN: _SceneCapabilities,
|
scene.DOMAIN: _SceneCapabilities,
|
||||||
script.DOMAIN: _ScriptCapabilities,
|
script.DOMAIN: _ScriptCapabilities,
|
||||||
switch.DOMAIN: _SwitchCapabilities,
|
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))
|
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.
|
"""Create a API formatted response message.
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
|
@ -435,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||||
if API_ENDPOINT in request:
|
if API_ENDPOINT in request:
|
||||||
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
||||||
|
|
||||||
|
if context is not None:
|
||||||
|
response[API_CONTEXT] = context
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -490,7 +535,12 @@ def async_api_discovery(hass, config, request):
|
||||||
'manufacturerName': 'Home Assistant',
|
'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)
|
discovery_endpoints.append(endpoint)
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
|
@ -976,3 +1026,25 @@ def async_api_previous(hass, config, request, entity):
|
||||||
data, blocking=False)
|
data, blocking=False)
|
||||||
|
|
||||||
return api_message(request)
|
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
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components import alexa
|
from homeassistant.components import alexa
|
||||||
from homeassistant.components.alexa import smart_home
|
from homeassistant.components.alexa import smart_home
|
||||||
|
@ -166,13 +167,27 @@ def test_discovery_request(hass):
|
||||||
'position': 85
|
'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(
|
msg = yield from smart_home.async_handle_message(
|
||||||
hass, DEFAULT_CONFIG, request)
|
hass, DEFAULT_CONFIG, request)
|
||||||
|
|
||||||
assert 'event' in msg
|
assert 'event' in msg
|
||||||
msg = msg['event']
|
msg = msg['event']
|
||||||
|
|
||||||
assert len(msg['payload']['endpoints']) == 16
|
assert len(msg['payload']['endpoints']) == 17
|
||||||
assert msg['header']['name'] == 'Discover.Response'
|
assert msg['header']['name'] == 'Discover.Response'
|
||||||
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
||||||
|
|
||||||
|
@ -334,6 +349,17 @@ def test_discovery_request(hass):
|
||||||
assert 'Alexa.PowerController' in caps
|
assert 'Alexa.PowerController' in caps
|
||||||
continue
|
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!")
|
raise AssertionError("Unknown appliance!")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1170,6 +1196,34 @@ def test_api_mute(hass, domain):
|
||||||
assert msg['header']['name'] == 'Response'
|
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
|
@asyncio.coroutine
|
||||||
def test_entity_config(hass):
|
def test_entity_config(hass):
|
||||||
"""Test that we can configure things via entity config."""
|
"""Test that we can configure things via entity config."""
|
||||||
|
|
Loading…
Reference in New Issue