diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi.py new file mode 100644 index 00000000000..eaf1508071a --- /dev/null +++ b/homeassistant/components/spaceapi.py @@ -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) diff --git a/tests/components/test_spaceapi.py b/tests/components/test_spaceapi.py new file mode 100644 index 00000000000..e7e7d158a31 --- /dev/null +++ b/tests/components/test_spaceapi.py @@ -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