core/homeassistant/components/frontend/__init__.py

295 lines
9.7 KiB
Python

"""Handle the frontend for Home Assistant."""
import asyncio
import hashlib
import json
import logging
import os
from aiohttp import web
from homeassistant.core import callback
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.components import api, group
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.auth import is_trusted_ip
from homeassistant.components.http.const import KEY_DEVELOPMENT
from .version import FINGERPRINTS
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
MANIFEST_JSON = {
"background_color": "#FFFFFF",
"description": "Open-source home automation platform running on Python 3.",
"dir": "ltr",
"display": "standalone",
"icons": [],
"lang": "en-US",
"name": "Home Assistant",
"short_name": "Assistant",
"start_url": "/",
"theme_color": "#03A9F4"
}
for size in (192, 384, 512, 1024):
MANIFEST_JSON['icons'].append({
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
"sizes": "{}x{}".format(size, size),
"type": "image/png"
})
DATA_PANELS = 'frontend_panels'
DATA_INDEX_VIEW = 'frontend_index_view'
# To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel."""
nondev_path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.http.development:
url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
path = os.path.join(
STATIC_PATH, 'home-assistant-polymer/panels/',
'{0}/ha-panel-{0}.html'.format(component_name))
else:
url = None # use default url generate mechanism
path = os.path.join(STATIC_PATH, nondev_path)
# Fingerprint doesn't exist when adding new built-in panel
register_panel(hass, component_name, path,
FINGERPRINTS.get(nondev_path, 'dev'), sidebar_title,
sidebar_icon, url_path, url, config)
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
sidebar_icon=None, url_path=None, url=None, config=None):
"""Register a panel for the frontend.
component_name: name of the web component
path: path to the HTML of the web component
md5: the md5 hash of the web component (for versioning, optional)
sidebar_title: title to show in the sidebar (optional)
sidebar_icon: icon to show next to title in sidebar (optional)
url_path: name to use in the url (defaults to component_name)
url: for the web component (for dev environment, optional)
config: config to be passed into the web component
Warning: this API will probably change. Use at own risk.
"""
panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}
if url_path is None:
url_path = component_name
if url_path in panels:
_LOGGER.warning('Overwriting component %s', url_path)
if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s',
component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = {
'url_path': url_path,
'component_name': component_name,
}
if sidebar_title:
data['title'] = sidebar_title
if sidebar_icon:
data['icon'] = sidebar_icon
if config is not None:
data['config'] = config
if url is not None:
data['url'] = url
else:
url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS:
hass.http.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url
panels[url_path] = data
# Register index view for this route if IndexView already loaded
# Otherwise it will be done during setup.
index_view = hass.data.get(DATA_INDEX_VIEW)
if index_view:
hass.http.app.router.add_route('get', '/{}'.format(url_path),
index_view.get)
def add_manifest_json_key(key, val):
"""Add a keyval to the manifest.json."""
MANIFEST_JSON[key] = val
def setup(hass, config):
"""Setup serving the frontend."""
hass.http.register_view(BootstrapView)
hass.http.register_view(ManifestJSONView)
if hass.http.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
hass.http.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), False)
hass.http.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt"))
hass.http.register_static_path("/static", STATIC_PATH)
local = hass.config.path('www')
if os.path.isdir(local):
hass.http.register_static_path("/local", local)
index_view = hass.data[DATA_INDEX_VIEW] = IndexView()
hass.http.register_view(index_view)
# Components have registered panels before frontend got setup.
# Now register their urls.
if DATA_PANELS in hass.data:
for url_path in hass.data[DATA_PANELS]:
hass.http.app.router.add_route('get', '/{}'.format(url_path),
index_view.get)
else:
hass.data[DATA_PANELS] = {}
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'):
register_built_in_panel(hass, panel)
return True
class BootstrapView(HomeAssistantView):
"""View to bootstrap frontend with all needed data."""
url = "/api/bootstrap"
name = "api:bootstrap"
@callback
def get(self, request):
"""Return all data needed to bootstrap Home Assistant."""
hass = request.app['hass']
return self.json({
'config': hass.config.as_dict(),
'states': hass.states.async_all(),
'events': api.async_events_json(hass),
'services': api.async_services_json(hass),
'panels': hass.data[DATA_PANELS],
})
class IndexView(HomeAssistantView):
"""Serve the frontend."""
url = '/'
name = "frontend:index"
requires_auth = False
extra_urls = ['/states', '/states/{entity_id}']
def __init__(self):
"""Initialize the frontend view."""
from jinja2 import FileSystemLoader, Environment
self.templates = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
)
)
@asyncio.coroutine
def get(self, request, entity_id=None):
"""Serve the index view."""
hass = request.app['hass']
if entity_id is not None:
state = hass.states.get(entity_id)
if (not state or state.domain != 'group' or
not state.attributes.get(group.ATTR_VIEW)):
return self.json_message('Entity not found', HTTP_NOT_FOUND)
if request.app[KEY_DEVELOPMENT]:
core_url = '/static/home-assistant-polymer/build/core.js'
compatibility_url = \
'/static/home-assistant-polymer/build/compatibility.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else:
core_url = '/static/core-{}.js'.format(
FINGERPRINTS['core.js'])
compatibility_url = '/static/compatibility-{}.js'.format(
FINGERPRINTS['compatibility.js'])
ui_url = '/static/frontend-{}.html'.format(
FINGERPRINTS['frontend.html'])
if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]
if panel == 'states':
panel_url = ''
else:
panel_url = hass.data[DATA_PANELS][panel]['url']
no_auth = 'true'
if hass.config.api.api_password:
# require password if set
no_auth = 'false'
if is_trusted_ip(request):
# bypass for trusted networks
no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = yield from hass.loop.run_in_executor(
None, self.templates.get_template, 'index.html')
# pylint is wrong
# pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render(
core_url=core_url, ui_url=ui_url,
compatibility_url=compatibility_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=hass.data[DATA_PANELS])
return web.Response(text=resp, content_type='text/html')
class ManifestJSONView(HomeAssistantView):
"""View to return a manifest.json."""
requires_auth = False
url = "/manifest.json"
name = "manifestjson"
@asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return web.Response(body=msg, content_type="application/manifest+json")