"""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 EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND from homeassistant.components import api, group from homeassistant.components.http import HomeAssistantView from .version import FINGERPRINTS DOMAIN = 'frontend' DEPENDENCIES = ['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') PANELS = {} 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" } # 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.""" 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)) else: url = None # use default url generate mechanism register_panel(hass, component_name, os.path.join(STATIC_PATH, path), FINGERPRINTS[path], 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. """ 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 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), 0) 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) 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) def register_frontend_index(event): """Register the frontend index urls. Done when Home Assistant is started so that all panels are known. """ hass.http.register_view(IndexView( hass, ['/{}'.format(name) for name in PANELS])) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) 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" }) 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.""" return self.json({ 'config': self.hass.config.as_dict(), 'states': self.hass.states.async_all(), 'events': api.async_events_json(self.hass), 'services': api.async_services_json(self.hass), 'panels': PANELS, }) class IndexView(HomeAssistantView): """Serve the frontend.""" url = '/' name = "frontend:index" requires_auth = False extra_urls = ['/states', '/states/{entity_id}'] def __init__(self, hass, extra_urls): """Initialize the frontend view.""" super().__init__(hass) from jinja2 import FileSystemLoader, Environment self.extra_urls = self.extra_urls + extra_urls 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.""" if entity_id is not None: state = self.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 self.hass.http.development: core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' else: core_url = '/static/core-{}.js'.format( FINGERPRINTS['core.js']) ui_url = '/static/frontend-{}.html'.format( FINGERPRINTS['frontend.html']) if request.path == '/': panel = 'states' else: panel = request.path.split('/')[1] panel_url = PANELS[panel]['url'] if panel != 'states' else '' no_auth = 'true' if self.hass.config.api.api_password: # require password if set no_auth = 'false' if self.hass.http.is_trusted_ip( self.hass.http.get_real_ip(request)): # bypass for trusted networks no_auth = 'true' icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) template = yield from self.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, no_auth=no_auth, icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], panel_url=panel_url, panels=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")