mirror of https://github.com/home-assistant/core
341 lines
10 KiB
Python
341 lines
10 KiB
Python
"""Support for the SpaceAPI."""
|
|
|
|
from contextlib import suppress
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
ATTR_ICON,
|
|
ATTR_LOCATION,
|
|
ATTR_NAME,
|
|
ATTR_STATE,
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
CONF_ADDRESS,
|
|
CONF_EMAIL,
|
|
CONF_ENTITY_ID,
|
|
CONF_LOCATION,
|
|
CONF_SENSORS,
|
|
CONF_STATE,
|
|
CONF_URL,
|
|
)
|
|
import homeassistant.core as ha
|
|
from homeassistant.core import HomeAssistant
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.typing import ConfigType
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
ATTR_ADDRESS = "address"
|
|
ATTR_SPACEFED = "spacefed"
|
|
ATTR_CAM = "cam"
|
|
ATTR_STREAM = "stream"
|
|
ATTR_FEEDS = "feeds"
|
|
ATTR_CACHE = "cache"
|
|
ATTR_PROJECTS = "projects"
|
|
ATTR_RADIO_SHOW = "radio_show"
|
|
ATTR_LAT = "lat"
|
|
ATTR_LON = "lon"
|
|
ATTR_API = "api"
|
|
ATTR_CLOSED = "closed"
|
|
ATTR_CONTACT = "contact"
|
|
ATTR_ISSUE_REPORT_CHANNELS = "issue_report_channels"
|
|
ATTR_LASTCHANGE = "lastchange"
|
|
ATTR_LOGO = "logo"
|
|
ATTR_OPEN = "open"
|
|
ATTR_SENSORS = "sensors"
|
|
ATTR_SPACE = "space"
|
|
ATTR_UNIT = "unit"
|
|
ATTR_URL = "url"
|
|
ATTR_VALUE = "value"
|
|
ATTR_SENSOR_LOCATION = "location"
|
|
|
|
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_SPACEFED = "spacefed"
|
|
CONF_SPACENET = "spacenet"
|
|
CONF_SPACESAML = "spacesaml"
|
|
CONF_SPACEPHONE = "spacephone"
|
|
CONF_CAM = "cam"
|
|
CONF_STREAM = "stream"
|
|
CONF_M4 = "m4"
|
|
CONF_MJPEG = "mjpeg"
|
|
CONF_USTREAM = "ustream"
|
|
CONF_FEEDS = "feeds"
|
|
CONF_FEED_BLOG = "blog"
|
|
CONF_FEED_WIKI = "wiki"
|
|
CONF_FEED_CALENDAR = "calendar"
|
|
CONF_FEED_FLICKER = "flicker"
|
|
CONF_FEED_TYPE = "type"
|
|
CONF_FEED_URL = "url"
|
|
CONF_CACHE = "cache"
|
|
CONF_CACHE_SCHEDULE = "schedule"
|
|
CONF_PROJECTS = "projects"
|
|
CONF_RADIO_SHOW = "radio_show"
|
|
CONF_RADIO_SHOW_NAME = "name"
|
|
CONF_RADIO_SHOW_URL = "url"
|
|
CONF_RADIO_SHOW_TYPE = "type"
|
|
CONF_RADIO_SHOW_START = "start"
|
|
CONF_RADIO_SHOW_END = "end"
|
|
CONF_LOGO = "logo"
|
|
CONF_PHONE = "phone"
|
|
CONF_SIP = "sip"
|
|
CONF_KEYMASTERS = "keymasters"
|
|
CONF_KEYMASTER_NAME = "name"
|
|
CONF_KEYMASTER_IRC_NICK = "irc_nick"
|
|
CONF_KEYMASTER_PHONE = "phone"
|
|
CONF_KEYMASTER_EMAIL = "email"
|
|
CONF_KEYMASTER_TWITTER = "twitter"
|
|
CONF_TWITTER = "twitter"
|
|
CONF_FACEBOOK = "facebook"
|
|
CONF_IDENTICA = "identica"
|
|
CONF_FOURSQUARE = "foursquare"
|
|
CONF_ML = "ml"
|
|
CONF_JABBER = "jabber"
|
|
CONF_ISSUE_MAIL = "issue_mail"
|
|
CONF_SPACE = "space"
|
|
CONF_TEMPERATURE = "temperature"
|
|
|
|
DATA_SPACEAPI = "data_spaceapi"
|
|
DOMAIN = "spaceapi"
|
|
|
|
ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_ISSUE_MAIL, CONF_ML, 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})
|
|
|
|
SPACEFED_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_SPACENET): cv.boolean,
|
|
vol.Optional(CONF_SPACESAML): cv.boolean,
|
|
vol.Optional(CONF_SPACEPHONE): cv.boolean,
|
|
}
|
|
)
|
|
|
|
STREAM_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_M4): cv.url,
|
|
vol.Optional(CONF_MJPEG): cv.url,
|
|
vol.Optional(CONF_USTREAM): cv.url,
|
|
}
|
|
)
|
|
|
|
FEED_SCHEMA = vol.Schema(
|
|
{vol.Optional(CONF_FEED_TYPE): cv.string, vol.Required(CONF_FEED_URL): cv.url}
|
|
)
|
|
|
|
FEEDS_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_FEED_BLOG): FEED_SCHEMA,
|
|
vol.Optional(CONF_FEED_WIKI): FEED_SCHEMA,
|
|
vol.Optional(CONF_FEED_CALENDAR): FEED_SCHEMA,
|
|
vol.Optional(CONF_FEED_FLICKER): FEED_SCHEMA,
|
|
}
|
|
)
|
|
|
|
CACHE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(CONF_CACHE_SCHEDULE): cv.matches_regex(
|
|
r"(m.02|m.05|m.10|m.15|m.30|h.01|h.02|h.04|h.08|h.12|d.01)"
|
|
)
|
|
}
|
|
)
|
|
|
|
RADIO_SHOW_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(CONF_RADIO_SHOW_NAME): cv.string,
|
|
vol.Required(CONF_RADIO_SHOW_URL): cv.url,
|
|
vol.Required(CONF_RADIO_SHOW_TYPE): cv.matches_regex(r"(mp3|ogg)"),
|
|
vol.Required(CONF_RADIO_SHOW_START): cv.string,
|
|
vol.Required(CONF_RADIO_SHOW_END): cv.string,
|
|
}
|
|
)
|
|
|
|
KEYMASTER_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_KEYMASTER_NAME): cv.string,
|
|
vol.Optional(CONF_KEYMASTER_IRC_NICK): cv.string,
|
|
vol.Optional(CONF_KEYMASTER_PHONE): cv.string,
|
|
vol.Optional(CONF_KEYMASTER_EMAIL): cv.string,
|
|
vol.Optional(CONF_KEYMASTER_TWITTER): cv.string,
|
|
}
|
|
)
|
|
|
|
CONTACT_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_EMAIL): cv.string,
|
|
vol.Optional(CONF_IRC): cv.string,
|
|
vol.Optional(CONF_ML): cv.string,
|
|
vol.Optional(CONF_PHONE): cv.string,
|
|
vol.Optional(CONF_TWITTER): cv.string,
|
|
vol.Optional(CONF_SIP): cv.string,
|
|
vol.Optional(CONF_FACEBOOK): cv.string,
|
|
vol.Optional(CONF_IDENTICA): cv.string,
|
|
vol.Optional(CONF_FOURSQUARE): cv.string,
|
|
vol.Optional(CONF_JABBER): cv.string,
|
|
vol.Optional(CONF_ISSUE_MAIL): cv.string,
|
|
vol.Optional(CONF_KEYMASTERS): vol.All(
|
|
cv.ensure_list, [KEYMASTER_SCHEMA], vol.Length(min=1)
|
|
),
|
|
},
|
|
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], cv.string: [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.Optional(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,
|
|
vol.Optional(CONF_SPACEFED): SPACEFED_SCHEMA,
|
|
vol.Optional(CONF_CAM): vol.All(
|
|
cv.ensure_list, [cv.url], vol.Length(min=1)
|
|
),
|
|
vol.Optional(CONF_STREAM): STREAM_SCHEMA,
|
|
vol.Optional(CONF_FEEDS): FEEDS_SCHEMA,
|
|
vol.Optional(CONF_CACHE): CACHE_SCHEMA,
|
|
vol.Optional(CONF_PROJECTS): vol.All(cv.ensure_list, [cv.url]),
|
|
vol.Optional(CONF_RADIO_SHOW): vol.All(
|
|
cv.ensure_list, [RADIO_SHOW_SCHEMA]
|
|
),
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""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"
|
|
|
|
@staticmethod
|
|
def get_sensor_data(hass, spaceapi, sensor):
|
|
"""Get data from a sensor."""
|
|
if not (sensor_state := hass.states.get(sensor)):
|
|
return None
|
|
sensor_data = {ATTR_NAME: sensor_state.name, ATTR_VALUE: sensor_state.state}
|
|
if ATTR_SENSOR_LOCATION in sensor_state.attributes:
|
|
sensor_data[ATTR_LOCATION] = sensor_state.attributes[ATTR_SENSOR_LOCATION]
|
|
else:
|
|
sensor_data[ATTR_LOCATION] = spaceapi[CONF_SPACE]
|
|
# Some sensors don't have a unit of measurement
|
|
if ATTR_UNIT_OF_MEASUREMENT in sensor_state.attributes:
|
|
sensor_data[ATTR_UNIT] = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
|
|
return sensor_data
|
|
|
|
@ha.callback
|
|
def get(self, request):
|
|
"""Get SpaceAPI data."""
|
|
hass = request.app[KEY_HASS]
|
|
spaceapi = dict(hass.data[DATA_SPACEAPI])
|
|
is_sensors = spaceapi.get("sensors")
|
|
|
|
location = {ATTR_LAT: hass.config.latitude, ATTR_LON: hass.config.longitude}
|
|
|
|
try:
|
|
location[ATTR_ADDRESS] = spaceapi[ATTR_LOCATION][CONF_ADDRESS]
|
|
except KeyError:
|
|
pass
|
|
except TypeError:
|
|
pass
|
|
|
|
state_entity = spaceapi["state"][ATTR_ENTITY_ID]
|
|
|
|
if (space_state := hass.states.get(state_entity)) is not None:
|
|
state = {
|
|
ATTR_OPEN: space_state.state != "off",
|
|
ATTR_LASTCHANGE: dt_util.as_timestamp(space_state.last_updated),
|
|
}
|
|
else:
|
|
state = {ATTR_OPEN: "null", ATTR_LASTCHANGE: 0}
|
|
|
|
with suppress(KeyError):
|
|
state[ATTR_ICON] = {
|
|
ATTR_OPEN: spaceapi["state"][CONF_ICON_OPEN],
|
|
ATTR_CLOSED: spaceapi["state"][CONF_ICON_CLOSED],
|
|
}
|
|
|
|
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],
|
|
}
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_CAM] = spaceapi[CONF_CAM]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_STREAM] = spaceapi[CONF_STREAM]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_FEEDS] = spaceapi[CONF_FEEDS]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_CACHE] = spaceapi[CONF_CACHE]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS]
|
|
|
|
with suppress(KeyError):
|
|
data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW]
|
|
|
|
if is_sensors is not None:
|
|
sensors = {}
|
|
for sensor_type in is_sensors:
|
|
sensors[sensor_type] = []
|
|
for sensor in spaceapi["sensors"][sensor_type]:
|
|
sensor_data = self.get_sensor_data(hass, spaceapi, sensor)
|
|
sensors[sensor_type].append(sensor_data)
|
|
data[ATTR_SENSORS] = sensors
|
|
|
|
return self.json(data)
|