Add SpaceAPI support (#14204)
* Add SpaceAPI support * Changes according PR comments * Add tests * Remove print * Minor changes
This commit is contained in:
parent
c06351f2a9
commit
fb501282cc
2 changed files with 288 additions and 0 deletions
175
homeassistant/components/spaceapi.py
Normal file
175
homeassistant/components/spaceapi.py
Normal file
|
@ -0,0 +1,175 @@
|
|||
"""
|
||||
Support for the SpaceAPI.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/spaceapi/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_ICON, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE,
|
||||
ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, CONF_EMAIL,
|
||||
CONF_ENTITY_ID, CONF_SENSORS, CONF_STATE, CONF_URL)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_API = 'api'
|
||||
ATTR_CLOSE = 'close'
|
||||
ATTR_CONTACT = 'contact'
|
||||
ATTR_ISSUE_REPORT_CHANNELS = 'issue_report_channels'
|
||||
ATTR_LASTCHANGE = 'lastchange'
|
||||
ATTR_LOGO = 'logo'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_OPEN = 'open'
|
||||
ATTR_SENSORS = 'sensors'
|
||||
ATTR_SPACE = 'space'
|
||||
ATTR_UNIT = 'unit'
|
||||
ATTR_URL = 'url'
|
||||
ATTR_VALUE = 'value'
|
||||
|
||||
CONF_CONTACT = 'contact'
|
||||
CONF_HUMIDITY = 'humidity'
|
||||
CONF_ICON_CLOSED = 'icon_closed'
|
||||
CONF_ICON_OPEN = 'icon_open'
|
||||
CONF_ICONS = 'icons'
|
||||
CONF_IRC = 'irc'
|
||||
CONF_ISSUE_REPORT_CHANNELS = 'issue_report_channels'
|
||||
CONF_LOCATION = 'location'
|
||||
CONF_LOGO = 'logo'
|
||||
CONF_MAILING_LIST = 'mailing_list'
|
||||
CONF_PHONE = 'phone'
|
||||
CONF_SPACE = 'space'
|
||||
CONF_TEMPERATURE = 'temperature'
|
||||
CONF_TWITTER = 'twitter'
|
||||
|
||||
DATA_SPACEAPI = 'data_spaceapi'
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = 'spaceapi'
|
||||
|
||||
ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER]
|
||||
|
||||
SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE]
|
||||
SPACEAPI_VERSION = 0.13
|
||||
|
||||
URL_API_SPACEAPI = '/api/spaceapi'
|
||||
|
||||
LOCATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ADDRESS): cv.string,
|
||||
}, required=True)
|
||||
|
||||
CONTACT_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_EMAIL): cv.string,
|
||||
vol.Optional(CONF_IRC): cv.string,
|
||||
vol.Optional(CONF_MAILING_LIST): cv.string,
|
||||
vol.Optional(CONF_PHONE): cv.string,
|
||||
vol.Optional(CONF_TWITTER): cv.string,
|
||||
}, required=False)
|
||||
|
||||
STATE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Inclusive(CONF_ICON_CLOSED, CONF_ICONS): cv.url,
|
||||
vol.Inclusive(CONF_ICON_OPEN, CONF_ICONS): cv.url,
|
||||
}, required=False)
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema(
|
||||
{vol.In(SENSOR_TYPES): [cv.entity_id]}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_CONTACT): CONTACT_SCHEMA,
|
||||
vol.Required(CONF_ISSUE_REPORT_CHANNELS):
|
||||
vol.All(cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)]),
|
||||
vol.Required(CONF_LOCATION): LOCATION_SCHEMA,
|
||||
vol.Required(CONF_LOGO): cv.url,
|
||||
vol.Required(CONF_SPACE): cv.string,
|
||||
vol.Required(CONF_STATE): STATE_SCHEMA,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_SENSORS): SENSOR_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the SpaceAPI with the HTTP interface."""
|
||||
hass.data[DATA_SPACEAPI] = config[DOMAIN]
|
||||
hass.http.register_view(APISpaceApiView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class APISpaceApiView(HomeAssistantView):
|
||||
"""View to provide details according to the SpaceAPI."""
|
||||
|
||||
url = URL_API_SPACEAPI
|
||||
name = 'api:spaceapi'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get SpaceAPI data."""
|
||||
hass = request.app['hass']
|
||||
spaceapi = dict(hass.data[DATA_SPACEAPI])
|
||||
is_sensors = spaceapi.get('sensors')
|
||||
|
||||
location = {
|
||||
ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS],
|
||||
ATTR_LATITUDE: hass.config.latitude,
|
||||
ATTR_LONGITUDE: hass.config.longitude,
|
||||
}
|
||||
|
||||
state_entity = spaceapi['state'][ATTR_ENTITY_ID]
|
||||
space_state = hass.states.get(state_entity)
|
||||
|
||||
if space_state is not None:
|
||||
state = {
|
||||
ATTR_OPEN: False if space_state.state == 'off' else True,
|
||||
ATTR_LASTCHANGE:
|
||||
dt_util.as_timestamp(space_state.last_updated),
|
||||
}
|
||||
else:
|
||||
state = {ATTR_OPEN: 'null', ATTR_LASTCHANGE: 0}
|
||||
|
||||
try:
|
||||
state[ATTR_ICON] = {
|
||||
ATTR_OPEN: spaceapi['state'][CONF_ICON_OPEN],
|
||||
ATTR_CLOSE: spaceapi['state'][CONF_ICON_CLOSED],
|
||||
}
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
data = {
|
||||
ATTR_API: SPACEAPI_VERSION,
|
||||
ATTR_CONTACT: spaceapi[CONF_CONTACT],
|
||||
ATTR_ISSUE_REPORT_CHANNELS: spaceapi[CONF_ISSUE_REPORT_CHANNELS],
|
||||
ATTR_LOCATION: location,
|
||||
ATTR_LOGO: spaceapi[CONF_LOGO],
|
||||
ATTR_SPACE: spaceapi[CONF_SPACE],
|
||||
ATTR_STATE: state,
|
||||
ATTR_URL: spaceapi[CONF_URL],
|
||||
}
|
||||
|
||||
if is_sensors is not None:
|
||||
sensors = {}
|
||||
for sensor_type in is_sensors:
|
||||
sensors[sensor_type] = []
|
||||
for sensor in spaceapi['sensors'][sensor_type]:
|
||||
sensor_state = hass.states.get(sensor)
|
||||
unit = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
|
||||
value = sensor_state.state
|
||||
sensor_data = {
|
||||
ATTR_LOCATION: spaceapi[CONF_SPACE],
|
||||
ATTR_NAME: sensor_state.name,
|
||||
ATTR_UNIT: unit,
|
||||
ATTR_VALUE: value,
|
||||
}
|
||||
sensors[sensor_type].append(sensor_data)
|
||||
data[ATTR_SENSORS] = sensors
|
||||
|
||||
return self.json(data)
|
113
tests/components/test_spaceapi.py
Normal file
113
tests/components/test_spaceapi.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
"""The tests for the Home Assistant SpaceAPI component."""
|
||||
# pylint: disable=protected-access
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from tests.common import mock_coro
|
||||
|
||||
from homeassistant.components.spaceapi import (
|
||||
DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
CONFIG = {
|
||||
DOMAIN: {
|
||||
'space': 'Home',
|
||||
'logo': 'https://home-assistant.io/logo.png',
|
||||
'url': 'https://home-assistant.io',
|
||||
'location': {'address': 'In your Home'},
|
||||
'contact': {'email': 'hello@home-assistant.io'},
|
||||
'issue_report_channels': ['email'],
|
||||
'state': {
|
||||
'entity_id': 'test.test_door',
|
||||
'icon_open': 'https://home-assistant.io/open.png',
|
||||
'icon_closed': 'https://home-assistant.io/close.png',
|
||||
},
|
||||
'sensors': {
|
||||
'temperature': ['test.temp1', 'test.temp2'],
|
||||
'humidity': ['test.hum1'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SENSOR_OUTPUT = {
|
||||
'temperature': [
|
||||
{
|
||||
'location': 'Home',
|
||||
'name': 'temp1',
|
||||
'unit': '°C',
|
||||
'value': '25'
|
||||
},
|
||||
{
|
||||
'location': 'Home',
|
||||
'name': 'temp2',
|
||||
'unit': '°C',
|
||||
'value': '23'
|
||||
},
|
||||
],
|
||||
'humidity': [
|
||||
{
|
||||
'location': 'Home',
|
||||
'name': 'hum1',
|
||||
'unit': '%',
|
||||
'value': '88'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(hass, aiohttp_client):
|
||||
"""Start the Home Assistant HTTP component."""
|
||||
with patch('homeassistant.components.spaceapi',
|
||||
return_value=mock_coro(True)):
|
||||
hass.loop.run_until_complete(
|
||||
async_setup_component(hass, 'spaceapi', CONFIG))
|
||||
|
||||
hass.states.async_set('test.temp1', 25,
|
||||
attributes={'unit_of_measurement': '°C'})
|
||||
hass.states.async_set('test.temp2', 23,
|
||||
attributes={'unit_of_measurement': '°C'})
|
||||
hass.states.async_set('test.hum1', 88,
|
||||
attributes={'unit_of_measurement': '%'})
|
||||
|
||||
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
|
||||
|
||||
|
||||
async def test_spaceapi_get(hass, mock_client):
|
||||
"""Test response after start-up Home Assistant."""
|
||||
resp = await mock_client.get(URL_API_SPACEAPI)
|
||||
assert resp.status == 200
|
||||
|
||||
data = await resp.json()
|
||||
|
||||
assert data['api'] == SPACEAPI_VERSION
|
||||
assert data['space'] == 'Home'
|
||||
assert data['contact']['email'] == 'hello@home-assistant.io'
|
||||
assert data['location']['address'] == 'In your Home'
|
||||
assert data['location']['latitude'] == 32.87336
|
||||
assert data['location']['longitude'] == -117.22743
|
||||
assert data['state']['open'] == 'null'
|
||||
assert data['state']['icon']['open'] == \
|
||||
'https://home-assistant.io/open.png'
|
||||
assert data['state']['icon']['close'] == \
|
||||
'https://home-assistant.io/close.png'
|
||||
|
||||
|
||||
async def test_spaceapi_state_get(hass, mock_client):
|
||||
"""Test response if the state entity was set."""
|
||||
hass.states.async_set('test.test_door', True)
|
||||
|
||||
resp = await mock_client.get(URL_API_SPACEAPI)
|
||||
assert resp.status == 200
|
||||
|
||||
data = await resp.json()
|
||||
assert data['state']['open'] == bool(1)
|
||||
|
||||
|
||||
async def test_spaceapi_sensors_get(hass, mock_client):
|
||||
"""Test the response for the sensors."""
|
||||
resp = await mock_client.get(URL_API_SPACEAPI)
|
||||
assert resp.status == 200
|
||||
|
||||
data = await resp.json()
|
||||
assert data['sensors'] == SENSOR_OUTPUT
|
Loading…
Add table
Reference in a new issue