mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Merge branch 'dev'
* dev: (48 commits) Update README.md Fix some frontend bugs Fix style issues Move frontend specific js to webcomponents Improve performance for history component Fix JSON serialisation bug Allow querying recorder for run info Entity IDs are now always lowercase Get State History support from Store JS Stade model renamed entity_id to entityId Ensure entity ids are always lower case Period of history now returns a spanning result Tweaks for the new drawer UI Fixed Flake8 error Added a drawer to the UI Added suport for Tellstick light. Assume dimable switch is a light Added suport for Tellstick light. Assume dimable switch is a light Added suport for Tellstick light. Assume dimable switch is a light Update to latest home-assistant-js Frontend now build on top of home-assistant-js ...
This commit is contained in:
commit
eea4bb7118
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
config/*
|
||||
!config/home-assistant.conf.default
|
||||
homeassistant/components/http/www_static/polymer/bower_components/*
|
||||
homeassistant/components/frontend/www_static/polymer/bower_components/*
|
||||
|
||||
# There is not a better solution afaik..
|
||||
!config/custom_components
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -10,3 +10,6 @@
|
||||
[submodule "homeassistant/external/noop"]
|
||||
path = homeassistant/external/noop
|
||||
url = https://github.com/balloob/noop.git
|
||||
[submodule "homeassistant/components/frontend/www_static/polymer/home-assistant-js"]
|
||||
path = homeassistant/components/frontend/www_static/polymer/home-assistant-js
|
||||
url = https://github.com/balloob/home-assistant-js.git
|
||||
|
@ -101,4 +101,4 @@ time_minutes=0
|
||||
time_seconds=0
|
||||
|
||||
execute_service=notify.notify
|
||||
execute_service_data={"message":"It's 4, time for beer!"}
|
||||
service_data={"message":"It's 4, time for beer!"}
|
||||
|
@ -15,8 +15,6 @@ import re
|
||||
import datetime as dt
|
||||
import functools as ft
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
|
||||
@ -314,6 +312,14 @@ class Event(object):
|
||||
self.data = data or {}
|
||||
self.origin = origin
|
||||
|
||||
def as_dict(self):
|
||||
""" Returns a dict representation of this Event. """
|
||||
return {
|
||||
'event_type': self.event_type,
|
||||
'data': dict(self.data),
|
||||
'origin': str(self.origin)
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
# pylint: disable=maybe-no-member
|
||||
if self.data:
|
||||
@ -355,7 +361,8 @@ class EventBus(object):
|
||||
|
||||
event = Event(event_type, event_data, origin)
|
||||
|
||||
_LOGGER.info("Bus:Handling %s", event)
|
||||
if event_type != EVENT_TIME_CHANGED:
|
||||
_LOGGER.info("Bus:Handling %s", event)
|
||||
|
||||
if not listeners:
|
||||
return
|
||||
@ -438,7 +445,7 @@ class State(object):
|
||||
"Invalid entity id encountered: {}. "
|
||||
"Format should be <domain>.<object_id>").format(entity_id))
|
||||
|
||||
self.entity_id = entity_id
|
||||
self.entity_id = entity_id.lower()
|
||||
self.state = state
|
||||
self.attributes = attributes or {}
|
||||
self.last_updated = dt.datetime.now()
|
||||
@ -501,7 +508,7 @@ class StateMachine(object):
|
||||
""" Helper class that tracks the state of different entities. """
|
||||
|
||||
def __init__(self, bus):
|
||||
self._states = CaseInsensitiveDict()
|
||||
self._states = {}
|
||||
self._bus = bus
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@ -511,7 +518,7 @@ class StateMachine(object):
|
||||
domain_filter = domain_filter.lower()
|
||||
|
||||
return [state.entity_id for key, state
|
||||
in self._states.lower_items()
|
||||
in self._states.items()
|
||||
if util.split_entity_id(key)[0] == domain_filter]
|
||||
else:
|
||||
return list(self._states.keys())
|
||||
@ -522,7 +529,7 @@ class StateMachine(object):
|
||||
|
||||
def get(self, entity_id):
|
||||
""" Returns the state of the specified entity. """
|
||||
state = self._states.get(entity_id)
|
||||
state = self._states.get(entity_id.lower())
|
||||
|
||||
# Make a copy so people won't mutate the state
|
||||
return state.copy() if state else None
|
||||
@ -539,6 +546,8 @@ class StateMachine(object):
|
||||
|
||||
def is_state(self, entity_id, state):
|
||||
""" Returns True if entity exists and is specified state. """
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
return (entity_id in self._states and
|
||||
self._states[entity_id].state == state)
|
||||
|
||||
@ -546,6 +555,8 @@ class StateMachine(object):
|
||||
""" Removes an entity from the state machine.
|
||||
|
||||
Returns boolean to indicate if an entity was removed. """
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
with self._lock:
|
||||
return self._states.pop(entity_id, None) is not None
|
||||
|
||||
@ -557,7 +568,7 @@ class StateMachine(object):
|
||||
If you just update the attributes and not the state, last changed will
|
||||
not be affected.
|
||||
"""
|
||||
|
||||
entity_id = entity_id.lower()
|
||||
new_state = str(new_state)
|
||||
attributes = attributes or {}
|
||||
|
||||
@ -572,8 +583,8 @@ class StateMachine(object):
|
||||
if not (same_state and same_attr):
|
||||
last_changed = old_state.last_changed if same_state else None
|
||||
|
||||
state = self._states[entity_id] = \
|
||||
State(entity_id, new_state, attributes, last_changed)
|
||||
state = State(entity_id, new_state, attributes, last_changed)
|
||||
self._states[entity_id] = state
|
||||
|
||||
event_data = {'entity_id': entity_id, 'new_state': state}
|
||||
|
||||
@ -603,7 +614,7 @@ class StateMachine(object):
|
||||
@ft.wraps(action)
|
||||
def state_listener(event):
|
||||
""" The listener that listens for specific state changes. """
|
||||
if event.data['entity_id'].lower() in entity_ids and \
|
||||
if event.data['entity_id'] in entity_ids and \
|
||||
'old_state' in event.data and \
|
||||
_matcher(event.data['old_state'].state, from_state) and \
|
||||
_matcher(event.data['new_state'].state, to_state):
|
||||
|
@ -78,8 +78,9 @@ def ensure_config_path(config_dir):
|
||||
if not os.path.isfile(config_path):
|
||||
try:
|
||||
with open(config_path, 'w') as conf:
|
||||
conf.write("[http]\n\n")
|
||||
conf.write("[frontend]\n\n")
|
||||
conf.write("[discovery]\n\n")
|
||||
conf.write("[recorder]\n\n")
|
||||
except IOError:
|
||||
print(('Fatal Error: No configuration file found and unable '
|
||||
'to write a default one to {}').format(config_path))
|
||||
|
@ -24,6 +24,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_component(hass, domain, config=None):
|
||||
""" Setup a component for Home Assistant. """
|
||||
# Check if already loaded
|
||||
if domain in hass.components:
|
||||
return
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
|
||||
@ -63,7 +69,7 @@ def from_config_dict(config, hass=None):
|
||||
|
||||
enable_logging(hass)
|
||||
|
||||
loader.prepare(hass)
|
||||
_ensure_loader_prepared(hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Convert it to defaultdict so components can always have config dict
|
||||
@ -140,3 +146,9 @@ def enable_logging(hass):
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
""" Ensure Home Assistant loader is prepared. """
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
259
homeassistant/components/api.py
Normal file
259
homeassistant/components/api.py
Normal file
@ -0,0 +1,259 @@
|
||||
"""
|
||||
homeassistant.components.api
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides a Rest API for Home Assistant.
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.helpers import TrackStates
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.const import (
|
||||
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS)
|
||||
|
||||
HTTP_OK = 200
|
||||
HTTP_CREATED = 201
|
||||
HTTP_MOVED_PERMANENTLY = 301
|
||||
HTTP_BAD_REQUEST = 400
|
||||
HTTP_UNAUTHORIZED = 401
|
||||
HTTP_NOT_FOUND = 404
|
||||
HTTP_METHOD_NOT_ALLOWED = 405
|
||||
HTTP_UNPROCESSABLE_ENTITY = 422
|
||||
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Register the API with the HTTP interface. """
|
||||
|
||||
if 'http' not in hass.components:
|
||||
_LOGGER.error('Dependency http is not loaded')
|
||||
return False
|
||||
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
# /states
|
||||
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_get_api_states_entity)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
|
||||
# /events
|
||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
_handle_api_post_events_event)
|
||||
|
||||
# /services
|
||||
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
||||
hass.http.register_path(
|
||||
'POST',
|
||||
re.compile((r'/api/services/'
|
||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
_handle_post_api_services_domain_service)
|
||||
|
||||
# /event_forwarding
|
||||
hass.http.register_path(
|
||||
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
||||
hass.http.register_path(
|
||||
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
||||
|
||||
# /components
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
handler.write_json_message("API running.")
|
||||
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
""" Returns a dict containing all entity ids and their state. """
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
|
||||
|
||||
def _handle_get_api_states_entity(handler, path_match, data):
|
||||
""" Returns the state of a specific entity. """
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
|
||||
if state:
|
||||
handler.write_json(state)
|
||||
else:
|
||||
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
def _handle_post_state_entity(handler, path_match, data):
|
||||
""" Handles updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
try:
|
||||
new_state = data['state']
|
||||
except KeyError:
|
||||
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
attributes = data['attributes'] if 'attributes' in data else None
|
||||
|
||||
is_new_state = handler.server.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
handler.server.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
|
||||
handler.write_json(
|
||||
state.as_dict(),
|
||||
status_code=status_code,
|
||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
|
||||
def _handle_get_api_events(handler, path_match, data):
|
||||
""" Handles getting overview of event listeners. """
|
||||
handler.write_json([{"event": key, "listener_count": value}
|
||||
for key, value
|
||||
in handler.server.hass.bus.listeners.items()])
|
||||
|
||||
|
||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
""" Handles firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/api/events/<event_type>
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
event_type = path_match.group('event_type')
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
handler.write_json_message(
|
||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
handler.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
|
||||
handler.write_json_message("Event {} fired.".format(event_type))
|
||||
|
||||
|
||||
def _handle_get_api_services(handler, path_match, data):
|
||||
""" Handles getting overview of services. """
|
||||
handler.write_json(
|
||||
[{"domain": key, "services": value}
|
||||
for key, value
|
||||
in handler.server.hass.services.services.items()])
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
""" Handles calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
|
||||
with TrackStates(handler.server.hass) as changed_states:
|
||||
handler.server.hass.services.call(domain, service, data, True)
|
||||
|
||||
handler.write_json(changed_states)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(handler, path_match, data):
|
||||
""" Handles adding an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
handler.write_json_message(
|
||||
"No host or api_password received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
handler.write_json_message(
|
||||
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if handler.server.event_forwarder is None:
|
||||
handler.server.event_forwarder = \
|
||||
rem.EventForwarder(handler.server.hass)
|
||||
|
||||
handler.server.event_forwarder.connect(api)
|
||||
|
||||
handler.write_json_message("Event forwarding setup.")
|
||||
|
||||
|
||||
def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
""" Handles deleting an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if handler.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
handler.server.event_forwarder.disconnect(api)
|
||||
|
||||
handler.write_json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
""" Returns all the loaded components. """
|
||||
|
||||
handler.write_json(handler.server.hass.components)
|
@ -123,7 +123,7 @@ def setup(hass, config):
|
||||
{'entity_id': switches[1:]}))
|
||||
|
||||
# Setup room groups
|
||||
group.setup_group(hass, 'living_room', lights[0:3] + switches[0:1])
|
||||
group.setup_group(hass, 'living room', lights[0:3] + switches[0:1])
|
||||
group.setup_group(hass, 'bedroom', [lights[3]] + switches[1:])
|
||||
|
||||
# Setup process
|
||||
|
102
homeassistant/components/frontend/__init__.py
Normal file
102
homeassistant/components/frontend/__init__.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""
|
||||
homeassistant.components.frontend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides a frontend for Home Assistant.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
|
||||
from . import version
|
||||
import homeassistant.util as util
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
|
||||
HTTP_OK = 200
|
||||
HTTP_CREATED = 201
|
||||
HTTP_MOVED_PERMANENTLY = 301
|
||||
HTTP_BAD_REQUEST = 400
|
||||
HTTP_UNAUTHORIZED = 401
|
||||
HTTP_NOT_FOUND = 404
|
||||
HTTP_METHOD_NOT_ALLOWED = 405
|
||||
HTTP_UNPROCESSABLE_ENTITY = 422
|
||||
|
||||
|
||||
URL_ROOT = "/"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup serving the frontend. """
|
||||
if 'http' not in hass.components:
|
||||
_LOGGER.error('Dependency http is not loaded')
|
||||
return False
|
||||
|
||||
hass.http.register_path('GET', URL_ROOT, _handle_get_root, False)
|
||||
|
||||
# Static files
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
|
||||
def write(txt):
|
||||
""" Helper to write text to the output. """
|
||||
handler.wfile.write((txt + "\n").encode("UTF-8"))
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
handler.end_headers()
|
||||
|
||||
if handler.server.development:
|
||||
app_url = "polymer/home-assistant.html"
|
||||
else:
|
||||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = (handler.server.api_password if handler.server.no_password_set
|
||||
else data.get('api_password', ''))
|
||||
|
||||
write(("<!doctype html>"
|
||||
"<html>"
|
||||
"<head><title>Home Assistant</title>"
|
||||
"<meta name='mobile-web-app-capable' content='yes'>"
|
||||
"<link rel='shortcut icon' href='/static/favicon.ico' />"
|
||||
"<link rel='icon' type='image/png' "
|
||||
" href='/static/favicon-192x192.png' sizes='192x192'>"
|
||||
"<meta name='viewport' content='width=device-width, "
|
||||
" user-scalable=no, initial-scale=1.0, "
|
||||
" minimum-scale=1.0, maximum-scale=1.0' />"
|
||||
"<meta name='theme-color' content='#03a9f4'>"
|
||||
"</head>"
|
||||
"<body fullbleed>"
|
||||
"<h3 id='init' align='center'>Initializing Home Assistant</h3>"
|
||||
"<script"
|
||||
" src='/static/webcomponents.min.js'></script>"
|
||||
"<link rel='import' href='/static/{}' />"
|
||||
"<home-assistant auth='{}'></home-assistant>"
|
||||
"</body></html>").format(app_url, auth))
|
||||
|
||||
|
||||
def _handle_get_static(handler, path_match, data):
|
||||
""" Returns a static file for the frontend. """
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
# Strip md5 hash out of frontend filename
|
||||
if re.match(r'^frontend-[A-Za-z0-9]{32}\.html$', req_file):
|
||||
req_file = "frontend.html"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "43699d5ec727d3444985a1028d21e0d9"
|
||||
VERSION = "db6b9c263c4be99af5b25b8c1cb20e57"
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
210
homeassistant/components/frontend/www_static/frontend.html
Normal file
210
homeassistant/components/frontend/www_static/frontend.html
Normal file
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"version": "0.1.0",
|
||||
"authors": [
|
||||
"Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
],
|
||||
"main": "splash-login.html",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
],
|
||||
"dependencies": {
|
||||
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.4",
|
||||
"font-roboto": "Polymer/font-roboto#~0.5.4",
|
||||
"core-header-panel": "polymer/core-header-panel#~0.5.4",
|
||||
"core-toolbar": "polymer/core-toolbar#~0.5.4",
|
||||
"core-tooltip": "Polymer/core-tooltip#~0.5.4",
|
||||
"core-menu": "polymer/core-menu#~0.5.4",
|
||||
"core-item": "Polymer/core-item#~0.5.4",
|
||||
"core-input": "Polymer/core-input#~0.5.4",
|
||||
"core-icons": "polymer/core-icons#~0.5.4",
|
||||
"core-image": "polymer/core-image#~0.5.4",
|
||||
"core-style": "polymer/core-style#~0.5.4",
|
||||
"paper-toast": "Polymer/paper-toast#~0.5.4",
|
||||
"paper-dialog": "Polymer/paper-dialog#~0.5.4",
|
||||
"paper-spinner": "Polymer/paper-spinner#~0.5.4",
|
||||
"paper-button": "Polymer/paper-button#~0.5.4",
|
||||
"paper-input": "Polymer/paper-input#~0.5.4",
|
||||
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.4",
|
||||
"paper-icon-button": "polymer/paper-icon-button#~0.5.4",
|
||||
"paper-menu-button": "polymer/paper-menu-button#~0.5.4",
|
||||
"paper-dropdown": "polymer/paper-dropdown#~0.5.4",
|
||||
"paper-item": "polymer/paper-item#~0.5.4",
|
||||
"paper-slider": "polymer/paper-slider#~0.5.4",
|
||||
"color-picker-element": "~0.0.2",
|
||||
"google-apis": "GoogleWebComponents/google-apis#~0.4.2",
|
||||
"core-drawer-panel": "polymer/core-drawer-panel#~0.5.4",
|
||||
"core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.4",
|
||||
"moment": "~2.9.0"
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-configurator" attributes="stateObj api" noscript>
|
||||
<polymer-element name="state-card-configurator" attributes="stateObj" noscript>
|
||||
<template>
|
||||
<style>
|
||||
.state {
|
@ -0,0 +1,47 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-display.html">
|
||||
<link rel="import" href="state-card-toggle.html">
|
||||
<link rel="import" href="state-card-thermostat.html">
|
||||
<link rel="import" href="state-card-configurator.html">
|
||||
|
||||
<polymer-element name="state-card-content" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='cardContainer'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
var cardContainer = this.$.cardContainer;
|
||||
|
||||
if (!newVal) {
|
||||
if (cardContainer.lastChild) {
|
||||
cardContainer.removeChild(cardContainer.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oldVal || oldVal.cardType != newVal.cardType) {
|
||||
if (cardContainer.lastChild) {
|
||||
cardContainer.removeChild(cardContainer.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + newVal.cardType);
|
||||
stateCard.stateObj = newVal;
|
||||
cardContainer.appendChild(stateCard);
|
||||
|
||||
} else {
|
||||
|
||||
cardContainer.lastChild.stateObj = newVal;
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,4 +1,3 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
@ -1,4 +1,3 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
@ -1,4 +1,3 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
|
||||
@ -7,20 +6,17 @@
|
||||
<polymer-element name="state-card-toggle" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
/* the splash while enabling */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
|
||||
color: #0091ea;
|
||||
paper-toggle-button::shadow .toggle-ink {
|
||||
color: #039be5;
|
||||
}
|
||||
|
||||
/* filling of circle when checked */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
|
||||
paper-toggle-button::shadow [checked] .toggle-bar {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
/* line when checked */
|
||||
paper-toggle-button::shadow #toggleBar[checked] {
|
||||
paper-toggle-button::shadow [checked] .toggle-button {
|
||||
background-color: #039be5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
@ -41,6 +37,10 @@
|
||||
'stateObj.state': 'stateChanged'
|
||||
},
|
||||
|
||||
ready: function() {
|
||||
this.forceStateChange = this.forceStateChange.bind(this);
|
||||
},
|
||||
|
||||
// prevent the event from propegating
|
||||
toggleClicked: function(ev) {
|
||||
ev.stopPropagation();
|
||||
@ -63,16 +63,17 @@
|
||||
this.toggleChecked = newVal === "on";
|
||||
},
|
||||
|
||||
forceStateChange: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
},
|
||||
|
||||
turn_on: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
this.api.turn_on(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
window.hass.serviceActions.callTurnOn(this.stateObj.entityId)
|
||||
.then(this.forceStateChange);
|
||||
},
|
||||
|
||||
turn_off: function() {
|
||||
@ -80,11 +81,8 @@
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
this.api.turn_off(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
window.hass.serviceActions.callTurnOff(this.stateObj.entityId)
|
||||
.then(this.forceStateChange);
|
||||
},
|
||||
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
|
||||
<link rel="import" href="state-card-content.html">
|
||||
|
||||
<polymer-element name="state-card" attributes="api stateObj" on-click="cardClicked">
|
||||
<polymer-element name="state-card" attributes="stateObj" on-click="cardClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@ -19,13 +19,13 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<state-card-content stateObj={{stateObj}} api={{api}}></state-card-content>
|
||||
<state-card-content stateObj={{stateObj}}></state-card-content>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
cardClicked: function() {
|
||||
this.api.showmoreInfoDialog(this.stateObj.entity_id);
|
||||
window.hass.uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||
},
|
||||
|
||||
});
|
@ -21,7 +21,7 @@
|
||||
return "home";
|
||||
|
||||
case "group":
|
||||
return "social:communities";
|
||||
return "homeassistant-24:group";
|
||||
|
||||
case "device_tracker":
|
||||
return "social:person";
|
||||
@ -57,7 +57,7 @@
|
||||
return "announcement";
|
||||
|
||||
case "thermostat":
|
||||
return "homeassistant:thermostat";
|
||||
return "homeassistant-100:thermostat";
|
||||
|
||||
case "sensor":
|
||||
return "visibility";
|
@ -1,6 +1,6 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="entity-list" attributes="api cbEntityClicked">
|
||||
<polymer-element name="entity-list" attributes="cbEntityClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@ -24,7 +24,7 @@
|
||||
<div>
|
||||
<template repeat="{{state in states}}">
|
||||
<div class='eventContainer'>
|
||||
<a on-click={{handleClick}}>{{state.entity_id}}</a>
|
||||
<a on-click={{handleClick}}>{{state.entityId}}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -35,13 +35,18 @@
|
||||
cbEventClicked: null,
|
||||
states: [],
|
||||
|
||||
domReady: function() {
|
||||
this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
|
||||
this.statesUpdated()
|
||||
ready: function() {
|
||||
this.statesUpdated = this.statesUpdated.bind(this);
|
||||
window.hass.stateStore.addChangeListener(this.statesUpdated);
|
||||
this.statesUpdated();
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
window.hass.stateStore.removeChangeListener(this.statesUpdated);
|
||||
},
|
||||
|
||||
statesUpdated: function() {
|
||||
this.states = this.api.states;
|
||||
this.states = window.hass.stateStore.all();
|
||||
},
|
||||
|
||||
handleClick: function(ev) {
|
@ -1,6 +1,6 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="events-list" attributes="api cbEventClicked">
|
||||
<polymer-element name="events-list" attributes="cbEventClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@ -37,14 +37,18 @@
|
||||
cbEventClicked: null,
|
||||
events: [],
|
||||
|
||||
domReady: function() {
|
||||
this.events = this.api.events
|
||||
ready: function() {
|
||||
this.eventsUpdated = this.eventsUpdated.bind(this);
|
||||
window.hass.eventStore.addChangeListener(this.eventsUpdated);
|
||||
this.eventsUpdated();
|
||||
},
|
||||
|
||||
this.api.addEventListener('events-updated', this.eventsUpdated.bind(this))
|
||||
detached: function() {
|
||||
window.hass.eventStore.removeChangeListener(this.eventsUpdated);
|
||||
},
|
||||
|
||||
eventsUpdated: function() {
|
||||
this.events = this.api.events;
|
||||
this.events = window.hass.eventStore.all();
|
||||
},
|
||||
|
||||
handleClick: function(ev) {
|
@ -0,0 +1,25 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<polymer-element name="loading-box" attributes="text">
|
||||
<template>
|
||||
<style>
|
||||
.text {
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
vertical-align: top;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
<div layout='horizontal'>
|
||||
<paper-spinner active="true"></paper-spinner>
|
||||
<div class='text'>{{text}}…</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
text: "Loading"
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,47 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="./loading-box.html">
|
||||
|
||||
<polymer-element name="recent-states" attributes="stateObj">
|
||||
<template>
|
||||
<core-style ref='ha-data-table'></core-style>
|
||||
|
||||
<template if="{{recentStates === null}}">
|
||||
<loading-box text="Loading recent states"></loading-box>
|
||||
</template>
|
||||
|
||||
<template if="{{recentStates !== null}}">
|
||||
<div layout vertical>
|
||||
<template repeat="{{recentStates as state}}">
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div>
|
||||
{{state.state}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
{{state.last_changed | relativeHATime}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template if="{{recentStates.length == 0}}">
|
||||
There are no recent states.
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
recentStates: null,
|
||||
|
||||
stateObjChanged: function() {
|
||||
this.recentStates = null;
|
||||
|
||||
window.hass.callApi(
|
||||
'GET', 'history/entity/' + this.stateObj.entityId + '/recent_states').then(
|
||||
function(states) {
|
||||
this.recentStates = states.slice(1);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -5,7 +5,7 @@
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
|
||||
<polymer-element name="services-list" attributes="api cbServiceClicked">
|
||||
<polymer-element name="services-list" attributes="cbServiceClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@ -51,18 +51,22 @@
|
||||
services: [],
|
||||
cbServiceClicked: null,
|
||||
|
||||
domReady: function() {
|
||||
this.services = this.api.services
|
||||
ready: function() {
|
||||
this.servicesUpdated = this.servicesUpdated.bind(this);
|
||||
window.hass.serviceStore.addChangeListener(this.servicesUpdated);
|
||||
this.servicesUpdated();
|
||||
},
|
||||
|
||||
this.api.addEventListener('services-updated', this.servicesUpdated.bind(this))
|
||||
detached: function() {
|
||||
window.hass.serviceStore.removeChangeListener(this.servicesUpdated);
|
||||
},
|
||||
|
||||
getIcon: function(domain) {
|
||||
return (new DomainIcon).icon(domain);
|
||||
return (new DomainIcon()).icon(domain);
|
||||
},
|
||||
|
||||
servicesUpdated: function() {
|
||||
this.services = this.api.services;
|
||||
this.services = window.hass.serviceStore.all();
|
||||
},
|
||||
|
||||
serviceClicked: function(ev) {
|
@ -2,7 +2,7 @@
|
||||
|
||||
<link rel="import" href="../cards/state-card.html">
|
||||
|
||||
<polymer-element name="state-cards" attributes="api filter">
|
||||
<polymer-element name="state-cards" attributes="states">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@ -10,9 +10,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 764px) {
|
||||
@media all and (min-width: 1020px) {
|
||||
:host {
|
||||
padding-bottom: 8px;
|
||||
/*padding-bottom: 8px;*/
|
||||
}
|
||||
|
||||
.state-card {
|
||||
@ -21,14 +21,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1100px) {
|
||||
@media all and (min-width: 1356px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1450px) {
|
||||
@media all and (min-width: 1706px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
@ -47,7 +47,7 @@
|
||||
<div horizontal layout wrap>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card class="state-card" stateObj={{state}} api={{api}}></state-card>
|
||||
<state-card class="state-card" stateObj={{state}}></state-card>
|
||||
</template>
|
||||
|
||||
<template if="{{states.length == 0}}">
|
||||
@ -60,24 +60,6 @@
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
filter: null,
|
||||
states: [],
|
||||
|
||||
observe: {
|
||||
'api.states': 'filterChanged'
|
||||
},
|
||||
|
||||
filterChanged: function() {
|
||||
if(this.filter === 'customgroup') {
|
||||
this.states = this.api.getCustomGroups();
|
||||
} else {
|
||||
// if no filter, return all non-group states
|
||||
this.states = this.api.states.filter(function(state) {
|
||||
return state.domain != 'group';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,110 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/google-apis/google-jsapi.html">
|
||||
|
||||
<polymer-element name="state-timeline" attributes="stateHistory">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<google-jsapi on-api-load="{{googleApiLoaded}}"></google-jsapi>
|
||||
<div id="timeline" style='width: 100%; height: auto;'></div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
apiLoaded: false,
|
||||
stateHistory: null,
|
||||
|
||||
googleApiLoaded: function() {
|
||||
google.load("visualization", "1", {
|
||||
packages: ["timeline"],
|
||||
callback: function() {
|
||||
this.apiLoaded = true;
|
||||
this.drawChart();
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
stateHistoryChanged: function() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
drawChart: function() {
|
||||
if (!this.apiLoaded || !this.stateHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = this.$.timeline;
|
||||
var chart = new google.visualization.Timeline(container);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'string', id: 'Entity' });
|
||||
dataTable.addColumn({ type: 'string', id: 'State' });
|
||||
dataTable.addColumn({ type: 'date', id: 'Start' });
|
||||
dataTable.addColumn({ type: 'date', id: 'End' });
|
||||
|
||||
var addRow = function(entityDisplay, stateStr, start, end) {
|
||||
dataTable.addRow([entityDisplay, stateStr, start, end]);
|
||||
};
|
||||
|
||||
if (this.stateHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// people can pass in history of 1 entityId or a collection.
|
||||
|
||||
var stateHistory;
|
||||
if (_.isArray(this.stateHistory[0])) {
|
||||
stateHistory = this.stateHistory;
|
||||
} else {
|
||||
stateHistory = [this.stateHistory];
|
||||
}
|
||||
|
||||
// stateHistory is a list of lists of sorted state objects
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if(stateInfo.length === 0) return;
|
||||
|
||||
var entityDisplay = stateInfo[0].entityDisplay;
|
||||
var newLastChanged, prevState = null, prevLastChanged = null;
|
||||
|
||||
stateInfo.forEach(function(state) {
|
||||
if (prevState !== null && state.state !== prevState) {
|
||||
newLastChanged = state.lastChangedAsDate;
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
|
||||
|
||||
prevState = state.state;
|
||||
prevLastChanged = newLastChanged;
|
||||
} else if (prevState === null) {
|
||||
prevState = state.state;
|
||||
prevLastChanged = state.lastChangedAsDate;
|
||||
}
|
||||
});
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, new Date());
|
||||
}.bind(this));
|
||||
|
||||
chart.draw(dataTable, {
|
||||
height: 55 + stateHistory.length * 42,
|
||||
|
||||
// interactive properties require CSS, the JS api puts it on the document
|
||||
// instead of inside our Shadow DOM.
|
||||
enableInteractivity: false,
|
||||
|
||||
timeline: {
|
||||
showRowLabels: stateHistory.length > 1
|
||||
},
|
||||
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -2,12 +2,12 @@
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
<link rel="import" href="../components/state-timeline.html">
|
||||
<link rel="import" href="../more-infos/more-info-content.html">
|
||||
|
||||
<polymer-element name="more-info-dialog" attributes="api">
|
||||
<polymer-element name="more-info-dialog">
|
||||
<template>
|
||||
<ha-action-dialog id="dialog">
|
||||
|
||||
<style>
|
||||
.title-card {
|
||||
margin-bottom: 24px;
|
||||
@ -15,24 +15,50 @@
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<state-card-content stateObj="{{stateObj}}" api="{{api}}" class='title-card'>
|
||||
<state-card-content stateObj="{{stateObj}}" class='title-card'>
|
||||
</state-card-content>
|
||||
<more-info-content stateObj="{{stateObj}}" api="{{api}}"></more-info-content>
|
||||
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
|
||||
<more-info-content stateObj="{{stateObj}}"></more-info-content>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive on-click={{editClicked}}>Debug</paper-button>
|
||||
<paper-button affirmative>Dismiss</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObj: {},
|
||||
stateHistory: null,
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'reposition'
|
||||
},
|
||||
|
||||
ready: function() {
|
||||
this.stateHistoryStoreChanged = this.stateHistoryStoreChanged.bind(this);
|
||||
|
||||
window.hass.stateHistoryStore.addChangeListener(this.stateHistoryStoreChanged);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
window.hass.stateHistoryStore.removeChangeListener(this.stateHistoryStoreChanged);
|
||||
},
|
||||
|
||||
stateHistoryStoreChanged: function() {
|
||||
if (this.stateObj.entityId) {
|
||||
this.stateHistory = window.hass.stateHistoryStore.get(this.stateObj.entityId);
|
||||
} else {
|
||||
this.stateHistory = null;
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged: function() {
|
||||
if (this.stateObj.entityId &&
|
||||
window.hass.stateHistoryStore.isStale(this.stateObj.entityId)) {
|
||||
window.hass.stateHistoryActions.fetch(this.stateObj.entityId);
|
||||
}
|
||||
|
||||
this.stateHistoryStoreChanged();
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever the attributes change, the more info component can
|
||||
* hide or show elements. We will reposition the dialog.
|
||||
@ -53,10 +79,6 @@ Polymer({
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
editClicked: function(ev) {
|
||||
this.api.showEditStateDialog(this.stateObj.entity_id);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,110 @@
|
||||
<link rel="import" href="./bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<link rel="import" href="./dialogs/more-info-dialog.html">
|
||||
|
||||
<script src="./home-assistant-js/dist/homeassistant.min.js"></script>
|
||||
<script src="./bower_components/moment/moment.js"></script>
|
||||
|
||||
<script>
|
||||
var DOMAINS_WITH_CARD = ['thermostat', 'configurator'];
|
||||
var DOMAINS_WITH_MORE_INFO = ['light', 'group', 'sun', 'configurator'];
|
||||
|
||||
// Register some polymer filters
|
||||
PolymerExpressions.prototype.relativeHATime = function(timeString) {
|
||||
if (!timeString) return;
|
||||
|
||||
return moment(window.hass.util.parseDateTime(timeString)).fromNow();
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
|
||||
return (timeString || "").split(' ')[0];
|
||||
};
|
||||
|
||||
// Add some frontend specific helpers to the models
|
||||
Object.defineProperties(window.hass.stateModel.prototype, {
|
||||
// how to render the card for this state
|
||||
cardType: {
|
||||
get: function() {
|
||||
if(DOMAINS_WITH_CARD.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else if(this.canToggle) {
|
||||
return "toggle";
|
||||
} else {
|
||||
return "display";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// how to render the more info of this state
|
||||
moreInfoType: {
|
||||
get: function() {
|
||||
if(DOMAINS_WITH_MORE_INFO.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
relativeLastChanged: {
|
||||
get: function() {
|
||||
return moment(this.lastChangedAsDate).fromNow();
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<polymer-element name="home-assistant-api" attributes="auth">
|
||||
<template>
|
||||
<paper-toast id="toast" role="alert" text=""></paper-toast>
|
||||
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
var state,
|
||||
actions = window.hass.actions,
|
||||
dispatcher = window.hass.dispatcher;
|
||||
|
||||
var uiActions = window.hass.uiActions = {
|
||||
ACTION_SHOW_TOAST: actions.ACTION_SHOW_TOAST,
|
||||
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
|
||||
|
||||
showMoreInfoDialog: function(entityId) {
|
||||
dispatcher.dispatch({
|
||||
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
|
||||
entityId: entityId,
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
var getState = function(entityId) {
|
||||
return window.hass.stateStore.get(entityId);
|
||||
};
|
||||
|
||||
dispatcher.register(function(payload) {
|
||||
switch (payload.actionType) {
|
||||
case actions.ACTION_SHOW_TOAST:
|
||||
this.$.toast.text = payload.message;
|
||||
this.$.toast.show();
|
||||
break;
|
||||
|
||||
case uiActions.ACTION_SHOW_DIALOG_MORE_INFO:
|
||||
state = getState(payload.entityId);
|
||||
|
||||
this.$.moreInfoDialog.show(state);
|
||||
break;
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// if auth was given, tell the backend
|
||||
if(this.auth) {
|
||||
window.hass.authActions.validate(this.auth);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1 @@
|
||||
Subproject commit 4bba39bf1503bd98669cb3deba0d2e17bc03336d
|
@ -0,0 +1,49 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/font-roboto/roboto.html">
|
||||
|
||||
<link rel="import" href="resources/home-assistant-style.html">
|
||||
|
||||
<link rel="import" href="home-assistant-api.html">
|
||||
<link rel="import" href="layouts/login-form.html">
|
||||
<link rel="import" href="layouts/home-assistant-main.html">
|
||||
|
||||
<polymer-element name="home-assistant" attributes="auth">
|
||||
<template>
|
||||
<style>
|
||||
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<home-assistant-api auth="{{auth}}"></home-assistant-api>
|
||||
|
||||
<template if="{{!loaded}}">
|
||||
<login-form></login-form>
|
||||
</template>
|
||||
|
||||
<template if="{{loaded}}">
|
||||
<home-assistant-main></home-assistant-main>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
loaded: window.hass.syncStore.initialLoadDone(),
|
||||
|
||||
ready: function() {
|
||||
// remove the HTML init message
|
||||
document.getElementById('init').remove();
|
||||
|
||||
// listen if we are fully loaded
|
||||
window.hass.syncStore.addChangeListener(this.updateLoadStatus.bind(this));
|
||||
},
|
||||
|
||||
updateLoadStatus: function() {
|
||||
this.loaded = window.hass.syncStore.initialLoadDone();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,173 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-drawer-panel/core-drawer-panel.html">
|
||||
<link rel="import" href="../bower_components/core-header-panel/core-header-panel.html">
|
||||
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/paper-item/paper-item.html">
|
||||
|
||||
<link rel="import" href="../layouts/partial-states.html">
|
||||
<link rel="import" href="../layouts/partial-history.html">
|
||||
<link rel="import" href="../layouts/partial-dev-fire-event.html">
|
||||
<link rel="import" href="../layouts/partial-dev-call-service.html">
|
||||
<link rel="import" href="../layouts/partial-dev-set-state.html">
|
||||
|
||||
<polymer-element name="home-assistant-main">
|
||||
<template>
|
||||
<core-style ref="ha-headers"></core-style>
|
||||
|
||||
<style>
|
||||
core-header-panel {
|
||||
background: #fafafa;
|
||||
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
|
||||
color: #757575;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
core-menu core-icon {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
core-toolbar {
|
||||
font-weight: normal;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
core-menu {
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
min-height: 53px;
|
||||
}
|
||||
|
||||
.seperator {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.dev-tools {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<core-drawer-panel id="drawer" on-core-responsive-change="{{responsiveChanged}}">
|
||||
<core-header-panel mode="scroll" drawer>
|
||||
<core-toolbar>
|
||||
Home Assistant
|
||||
</core-toolbar>
|
||||
<core-menu id="menu"
|
||||
selected="0" excludedLocalNames="div" on-core-select="{{menuSelect}}"
|
||||
layout vertical>
|
||||
<paper-item data-panel="states">
|
||||
<core-icon icon="apps"></core-icon>
|
||||
States
|
||||
</paper-item>
|
||||
<paper-item data-panel="group">
|
||||
<core-icon icon="homeassistant-24:group"></core-icon>
|
||||
Groups
|
||||
</paper-item>
|
||||
<paper-item data-panel="history">
|
||||
<core-icon icon="assessment"></core-icon>
|
||||
History
|
||||
</paper-item>
|
||||
|
||||
<div flex></div>
|
||||
|
||||
<paper-item id="logout" on-click="{{handleLogOutClick}}">
|
||||
<core-icon icon="exit-to-app"></core-icon>
|
||||
Log Out
|
||||
</paper-item>
|
||||
|
||||
<div class='seperator'>Developer Tools</div>
|
||||
<div class='dev-tools' layout horizontal justified>
|
||||
<paper-icon-button
|
||||
icon="settings-remote" data-panel='call-service'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon="settings-ethernet" data-panel='set-state'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon="settings-input-antenna" data-panel='fire-event'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
</div>
|
||||
</core-menu>
|
||||
</core-header-panel>
|
||||
|
||||
<!--
|
||||
This is the main partial, never remove it from the DOM but hide it
|
||||
to speed up when people click on states.
|
||||
-->
|
||||
<partial-states hidden?="{{selected != 'states' && selected != 'group'}}"
|
||||
main narrow="{{narrow}}"
|
||||
togglePanel="{{togglePanel}}"
|
||||
filter="{{selected == 'group' ? 'group' : null}}">
|
||||
</partial-states>
|
||||
|
||||
<template if="{{selected == 'history'}}">
|
||||
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
|
||||
</template>
|
||||
<template if="{{selected == 'fire-event'}}">
|
||||
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
|
||||
</template>
|
||||
<template if="{{selected == 'set-state'}}">
|
||||
<partial-dev-set-state main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-set-state>
|
||||
</template>
|
||||
<template if="{{selected == 'call-service'}}">
|
||||
<partial-dev-call-service main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-call-service>
|
||||
</template>
|
||||
</core-drawer-panel>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
selected: "states",
|
||||
narrow: false,
|
||||
|
||||
ready: function() {
|
||||
this.togglePanel = this.togglePanel.bind(this);
|
||||
},
|
||||
|
||||
menuSelect: function(ev, detail, sender) {
|
||||
if (detail.isSelected) {
|
||||
this.selectPanel(detail.item);
|
||||
}
|
||||
},
|
||||
|
||||
handleDevClick: function(ev, detail, sender) {
|
||||
this.$.menu.selected = -1;
|
||||
this.selectPanel(ev.target);
|
||||
},
|
||||
|
||||
selectPanel: function(element) {
|
||||
var newChoice = element.dataset.panel;
|
||||
|
||||
if(newChoice !== this.selected) {
|
||||
this.togglePanel();
|
||||
this.selected = newChoice;
|
||||
}
|
||||
},
|
||||
|
||||
responsiveChanged: function(ev, detail, sender) {
|
||||
this.narrow = detail.narrow;
|
||||
},
|
||||
|
||||
togglePanel: function() {
|
||||
this.$.drawer.togglePanel();
|
||||
},
|
||||
|
||||
handleLogOutClick: function() {
|
||||
window.hass.authActions.logOut();
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,129 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/core-input/core-input.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<polymer-element name="login-form">
|
||||
<template>
|
||||
<style>
|
||||
paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login paper-button {
|
||||
margin-left: 242px;
|
||||
}
|
||||
|
||||
.login .interact {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
#validatebox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validatemessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout horizontal center fit class='login' id="splash">
|
||||
<div layout vertical center flex>
|
||||
|
||||
<img src="/static/favicon-192x192.png" />
|
||||
<h1>Home Assistant</h1>
|
||||
|
||||
<a href="#" id="hideKeyboardOnFocus"></a>
|
||||
|
||||
<div class='interact' layout vertical>
|
||||
|
||||
<div id='loginform' hidden?="{{isValidating || isLoggedIn}}">
|
||||
<paper-input-decorator label="Password" id="passwordDecorator">
|
||||
<input is="core-input" type="password" id="passwordInput"
|
||||
value="{{authToken}}" on-keyup="{{passwordKeyup}}">
|
||||
</paper-input-decorator>
|
||||
<paper-button on-click={{validatePassword}}>Log In</paper-button>
|
||||
</div>
|
||||
|
||||
<div id="validatebox" hidden?="{{!(isValidating || isLoggedIn)}}">
|
||||
<paper-spinner active="true"></paper-spinner><br />
|
||||
<div class="validatemessage">{{spinnerMessage}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
MSG_VALIDATING: "Validating password…",
|
||||
MSG_LOADING_DATA: "Loading data…",
|
||||
|
||||
authToken: "",
|
||||
|
||||
isValidating: false,
|
||||
isLoggedIn: false,
|
||||
|
||||
spinnerMessage: "",
|
||||
|
||||
ready: function() {
|
||||
this.authStore = window.hass.authStore;
|
||||
|
||||
this.authChangeListener = this.authChangeListener.bind(this);
|
||||
|
||||
this.authStore.addChangeListener(this.authChangeListener);
|
||||
|
||||
this.authChangeListener();
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.focusPassword();
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.authStore.removeChangeListener(this.authChangeListener);
|
||||
},
|
||||
|
||||
authChangeListener: function() {
|
||||
this.isValidating = this.authStore.isValidating();
|
||||
this.isLoggedIn = this.authStore.isLoggedIn();
|
||||
this.spinnerMessage = this.isValidating ? this.MSG_VALIDATING : this.MSG_LOADING_DATA;
|
||||
|
||||
if (this.authStore.wasLastAttemptInvalid()) {
|
||||
this.$.passwordDecorator.error = this.authStore.getLastAttemptMessage();
|
||||
this.$.passwordDecorator.isInvalid = true;
|
||||
}
|
||||
|
||||
if (!(this.isValidating && this.isLoggedIn)) {
|
||||
this.job('focusPasswordBox', this.focusPassword.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
focusPassword: function() {
|
||||
this.$.passwordInput.focus();
|
||||
},
|
||||
|
||||
passwordKeyup: function(ev) {
|
||||
// validate on enter
|
||||
if(ev.keyCode === 13) {
|
||||
this.validatePassword();
|
||||
|
||||
// clear error after we start typing again
|
||||
} else if(this.$.passwordDecorator.isInvalid) {
|
||||
this.$.passwordDecorator.isInvalid = false;
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: function() {
|
||||
this.$.hideKeyboardOnFocus.focus();
|
||||
|
||||
window.hass.authActions.validate(this.authToken);
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -0,0 +1,28 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-scroll-header-panel/core-scroll-header-panel.html">
|
||||
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
|
||||
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="partial-base" attributes="narrow togglePanel" noscript>
|
||||
<template>
|
||||
<core-style ref="ha-headers"></core-style>
|
||||
|
||||
<core-scroll-header-panel fit fixed="{{!narrow}}">
|
||||
<core-toolbar>
|
||||
<paper-icon-button
|
||||
id="navicon" icon="menu" hidden?="{{!narrow}}"
|
||||
on-click="{{togglePanel}}"></paper-icon-button>
|
||||
<div flex>
|
||||
<content select="[header-title]"></content>
|
||||
</div>
|
||||
<content select="[header-buttons]"></content>
|
||||
</core-toolbar>
|
||||
|
||||
<content></content>
|
||||
|
||||
</core-scroll-header-panel>
|
||||
</template>
|
||||
</polymer>
|
@ -0,0 +1,86 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/services-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-call-service" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Call Service</span>
|
||||
|
||||
<div class='form' fit>
|
||||
<p>
|
||||
Call a service from a component.
|
||||
</p>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Service Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
|
||||
</paper-input-decorator>
|
||||
<paper-button on-click={{clickCallService}}>Call Service</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available services:</b>
|
||||
<services-list cbServiceClicked={{serviceSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.serviceSelected = this.serviceSelected.bind(this);
|
||||
},
|
||||
|
||||
setService: function(domain, service) {
|
||||
this.$.inputDomain.value = domain;
|
||||
this.$.inputService.value = service;
|
||||
},
|
||||
|
||||
serviceSelected: function(domain, service) {
|
||||
this.setService(domain, service);
|
||||
},
|
||||
|
||||
clickCallService: function() {
|
||||
try {
|
||||
window.hass.serviceActions.callService(
|
||||
this.$.inputDomain.value,
|
||||
this.$.inputService.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,79 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/events-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-fire-event" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Fire Event</span>
|
||||
|
||||
<div class='form' fit>
|
||||
<p>
|
||||
Fire an event on the event bus.
|
||||
</p>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input
|
||||
id="inputType" label="Event Type" floatingLabel="true"
|
||||
autofocus required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Event Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
</paper-input-decorator>
|
||||
|
||||
<paper-button on-click={{clickFireEvent}}>Fire Event</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available events:</b>
|
||||
<events-list cbEventClicked={{eventSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.eventSelected = this.eventSelected.bind(this);
|
||||
},
|
||||
|
||||
eventSelected: function(eventType) {
|
||||
this.$.inputType.value = eventType;
|
||||
},
|
||||
|
||||
clickFireEvent: function() {
|
||||
try {
|
||||
window.hass.eventActions.fire(
|
||||
this.$.inputType.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,103 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/entity-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-set-state" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Set State</span>
|
||||
|
||||
<div class='form' fit>
|
||||
<div>
|
||||
Set the representation of a device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</div>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="State attributes (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
|
||||
</paper-input-decorator>
|
||||
|
||||
<paper-button on-click={{clickSetState}}>Set State</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Current entities:</b>
|
||||
<entity-list cbEntityClicked={{entitySelected}}></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.entitySelected = this.entitySelected.bind(this);
|
||||
},
|
||||
|
||||
setEntityId: function(entityId) {
|
||||
this.$.inputEntityID.value = entityId;
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.$.inputState.value = state;
|
||||
},
|
||||
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
this.$.inputData.value = value;
|
||||
|
||||
// not according to the spec but it works...
|
||||
this.$.inputDataWrapper.update(this.$.inputData);
|
||||
},
|
||||
|
||||
entitySelected: function(entityId) {
|
||||
this.setEntityId(entityId);
|
||||
|
||||
var state = window.hass.stateStore.get(entityId);
|
||||
this.setState(state.state);
|
||||
this.setStateData(state.attributes);
|
||||
},
|
||||
|
||||
clickSetState: function(ev) {
|
||||
try {
|
||||
window.hass.stateActions.set(
|
||||
this.$.inputEntityID.value,
|
||||
this.$.inputState.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
|
||||
);
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,60 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/state-timeline.html">
|
||||
|
||||
<polymer-element name="partial-history" attributes="narrow togglePanel">
|
||||
<template>
|
||||
<style>
|
||||
.content {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.content.wide {
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>History</span>
|
||||
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh"
|
||||
on-click="{{handleRefreshClick}}"></paper-icon-button>
|
||||
</span>
|
||||
|
||||
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
|
||||
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateHistory: null,
|
||||
|
||||
ready: function() {
|
||||
this.stateHistoryStoreChanged = this.stateHistoryStoreChanged.bind(this);
|
||||
|
||||
window.hass.stateHistoryStore.addChangeListener(this.stateHistoryStoreChanged);
|
||||
|
||||
if (window.hass.stateHistoryStore.isStale()) {
|
||||
window.hass.stateHistoryActions.fetchAll();
|
||||
}
|
||||
|
||||
this.stateHistoryStoreChanged();
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
window.hass.stateHistoryStore.removeChangeListener(this.stateHistoryStoreChanged);
|
||||
},
|
||||
|
||||
stateHistoryStoreChanged: function() {
|
||||
this.stateHistory = window.hass.stateHistoryStore.all();
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
window.hass.stateHistoryActions.fetchAll();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer>
|
@ -0,0 +1,90 @@
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/state-cards.html">
|
||||
|
||||
<polymer-element name="partial-states" attributes="narrow togglePanel filter">
|
||||
<template>
|
||||
<core-style ref="ha-animations"></core-style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>{{headerTitle}}</span>
|
||||
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
|
||||
on-click="{{handleRefreshClick}}"></paper-icon-button>
|
||||
</span>
|
||||
|
||||
<state-cards states="{{states}}">
|
||||
<h3>Hi there!</h3>
|
||||
<p>
|
||||
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
|
||||
</p>
|
||||
<p>
|
||||
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
|
||||
</p>
|
||||
</state-cards>
|
||||
</partial-base>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
headerTitle: "States",
|
||||
states: [],
|
||||
isFetching: false,
|
||||
|
||||
ready: function() {
|
||||
this.stateStoreChanged = this.stateStoreChanged.bind(this);
|
||||
window.hass.stateStore.addChangeListener(this.stateStoreChanged);
|
||||
|
||||
this.syncStoreChanged = this.syncStoreChanged.bind(this);
|
||||
window.hass.syncStore.addChangeListener(this.syncStoreChanged);
|
||||
|
||||
this.refreshStates();
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
window.hass.stateStore.removeChangeListener(this.stateStoreChanged);
|
||||
window.hass.syncStore.removeChangeListener(this.syncStoreChanged);
|
||||
},
|
||||
|
||||
stateStoreChanged: function() {
|
||||
this.refreshStates();
|
||||
},
|
||||
|
||||
syncStoreChanged: function() {
|
||||
this.isFetching = window.hass.syncStore.isFetching();
|
||||
},
|
||||
|
||||
filterChanged: function() {
|
||||
this.refreshStates();
|
||||
|
||||
switch (this.filter) {
|
||||
case "group":
|
||||
this.headerTitle = "Groups";
|
||||
break;
|
||||
|
||||
default:
|
||||
this.headerTitle = "States";
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
refreshStates: function() {
|
||||
if (this.filter == 'group') {
|
||||
this.states = _.filter(window.hass.stateStore.all(), function(state) {
|
||||
return state.domain === 'group';
|
||||
});
|
||||
} else {
|
||||
this.states = _.filter(window.hass.stateStore.all(), function(state) {
|
||||
return state.domain !== 'group';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
window.hass.syncActions.sync();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer>
|
@ -76,15 +76,16 @@
|
||||
configure_id: this.stateObj.attributes.configure_id
|
||||
};
|
||||
|
||||
this.api.call_service('configurator', 'configure', data, {
|
||||
success: function() {
|
||||
window.hass.serviceActions.callService('configurator', 'configure', data).then(
|
||||
|
||||
function() {
|
||||
this.action = 'display';
|
||||
this.api.fetchAll();
|
||||
window.hass.syncActions.sync();
|
||||
}.bind(this),
|
||||
error: function() {
|
||||
|
||||
function() {
|
||||
this.action = 'display';
|
||||
}.bind(this)
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,60 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="more-info-default.html">
|
||||
<link rel="import" href="more-info-light.html">
|
||||
<link rel="import" href="more-info-group.html">
|
||||
<link rel="import" href="more-info-sun.html">
|
||||
<link rel="import" href="more-info-configurator.html">
|
||||
|
||||
<polymer-element name="more-info-content" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='moreInfoContainer' class='{{classNames}}'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
classNames: '',
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'stateAttributesChanged',
|
||||
},
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
var moreInfoContainer = this.$.moreInfoContainer;
|
||||
|
||||
if (!newVal) {
|
||||
if (moreInfoContainer.lastChild) {
|
||||
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oldVal || oldVal.moreInfoType != newVal.moreInfoType) {
|
||||
if (moreInfoContainer.lastChild) {
|
||||
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
|
||||
}
|
||||
|
||||
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
|
||||
moreInfo.stateObj = newVal;
|
||||
moreInfoContainer.appendChild(moreInfo);
|
||||
|
||||
} else {
|
||||
|
||||
moreInfoContainer.lastChild.stateObj = newVal;
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
stateAttributesChanged: function(oldVal, newVal) {
|
||||
this.classNames = Object.keys(newVal).map(
|
||||
function(key) { return "has-" + key; }).join(' ');
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,20 +1,11 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="more-info-default" attributes="stateObj">
|
||||
<polymer-element name="more-info-default" attributes="stateObj api">
|
||||
<template>
|
||||
<core-style ref='ha-key-value-table'></core-style>
|
||||
<style>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
padding-left: 10px;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
@ -23,22 +14,21 @@
|
||||
|
||||
<template repeat="{{key in stateObj.attributes | getKeys}}">
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div>
|
||||
<div class='key'>
|
||||
{{key}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
<div class='value'>
|
||||
{{stateObj.attributes[key]}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
getKeys: function(obj) {
|
||||
return Object.keys(obj || {});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -2,7 +2,7 @@
|
||||
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
|
||||
<polymer-element name="more-info-group" attributes="stateObj api">
|
||||
<polymer-element name="more-info-group" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
.child-card {
|
||||
@ -15,7 +15,7 @@
|
||||
</style>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card-content stateObj="{{state}}" api="{{api}}" class='child-card'>
|
||||
<state-card-content stateObj="{{state}}" class='child-card'>
|
||||
</state-card-content>
|
||||
</template>
|
||||
</template>
|
||||
@ -26,7 +26,7 @@ Polymer({
|
||||
},
|
||||
|
||||
updateStates: function() {
|
||||
this.states = this.api.getStates(this.stateObj.attributes.entity_id);
|
||||
this.states = window.hass.stateStore.gets(this.stateObj.attributes.entity_id);
|
||||
}
|
||||
});
|
||||
</script>
|
@ -56,9 +56,8 @@
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
// on-change is unpredictable so using on-core-change this has side effect
|
||||
// that it fires if changed by brightnessChanged(), thus an ignore boolean.
|
||||
ignoreNextBrightnessEvent: false,
|
||||
// initial change should be ignored
|
||||
ignoreNextBrightnessEvent: true,
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes.brightness': 'brightnessChanged',
|
||||
@ -68,7 +67,7 @@ Polymer({
|
||||
brightnessChanged: function(oldVal, newVal) {
|
||||
this.ignoreNextBrightnessEvent = true;
|
||||
|
||||
this.$.brightness.value = newVal;
|
||||
this.$.brightness.value = newVal;
|
||||
},
|
||||
|
||||
domReady: function() {
|
||||
@ -86,10 +85,10 @@ Polymer({
|
||||
if(isNaN(bri)) return;
|
||||
|
||||
if(bri === 0) {
|
||||
this.api.turn_off(this.stateObj.entity_id);
|
||||
window.hass.serviceActions.callTurnOff(this.stateObj.entityId);
|
||||
} else {
|
||||
this.api.call_service("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
window.hass.serviceActions.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entityId,
|
||||
brightness: bri
|
||||
});
|
||||
}
|
||||
@ -98,8 +97,8 @@ Polymer({
|
||||
colorPicked: function(ev) {
|
||||
var color = ev.detail.rgb;
|
||||
|
||||
this.api.call_service("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
window.hass.serviceActions.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entityId,
|
||||
rgb_color: [color.r, color.g, color.b]
|
||||
});
|
||||
}
|
@ -1,42 +1,26 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="more-info-sun" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
<core-style ref='ha-key-value-table'></core-style>
|
||||
|
||||
<div layout vertical id='sunData'>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='rising'>
|
||||
<div>
|
||||
<div class='key'>
|
||||
Rising {{stateObj.attributes.next_rising | relativeHATime}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
<div class='value'>
|
||||
{{stateObj.attributes.next_rising | HATimeStripDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='setting'>
|
||||
<div>
|
||||
<div class='key'>
|
||||
Setting {{stateObj.attributes.next_setting | relativeHATime}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
<div class='value'>
|
||||
{{stateObj.attributes.next_setting | HATimeStripDate}}
|
||||
</div>
|
||||
</div>
|
||||
@ -47,8 +31,8 @@
|
||||
Polymer({
|
||||
|
||||
stateObjChanged: function() {
|
||||
var rising = ha.util.parseTime(this.stateObj.attributes.next_rising);
|
||||
var setting = ha.util.parseTime(this.stateObj.attributes.next_setting);
|
||||
var rising = window.hass.util.parseDateTime(this.stateObj.attributes.next_rising);
|
||||
var setting = window.hass.util.parseDateTime(this.stateObj.attributes.next_setting);
|
||||
|
||||
if(rising > setting) {
|
||||
this.$.sunData.appendChild(this.$.rising);
|
@ -0,0 +1,28 @@
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
|
||||
|
||||
<core-iconset-svg id="homeassistant-100" iconSize="100">
|
||||
<svg><defs>
|
||||
<g id="thermostat">
|
||||
<!--
|
||||
Thermostat icon created by Scott Lewis from the Noun Project
|
||||
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
|
||||
-->
|
||||
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
||||
|
||||
<core-iconset-svg id="homeassistant-24" iconSize="24">
|
||||
<svg><defs>
|
||||
<!--
|
||||
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
-->
|
||||
<g id="group"><path d="M9 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5-3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-7c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
|
||||
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
@ -0,0 +1,113 @@
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<core-style id='ha-animations'>
|
||||
@-webkit-keyframes ha-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@keyframes ha-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ha-spin {
|
||||
-webkit-animation: ha-spin 2s infinite linear;
|
||||
animation: ha-spin 2s infinite linear;
|
||||
}
|
||||
</core-style>
|
||||
<core-style id="ha-headers">
|
||||
core-scroll-header-panel, core-header-panel {
|
||||
background-color: #E5E5E5;
|
||||
}
|
||||
|
||||
core-toolbar {
|
||||
background: #03a9f4;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id="ha-dialog">
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
|
||||
min-width: 350px;
|
||||
max-width: 700px;
|
||||
|
||||
/* First two are from core-transition-bottom */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
opacity 0.2s ease-in,
|
||||
top .3s,
|
||||
left .3s !important;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
:host.two-column {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 464px) {
|
||||
:host {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id='ha-key-value-table'>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data-entry .key {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
</core-style>
|
14
homeassistant/components/frontend/www_static/webcomponents.min.js
vendored
Normal file
14
homeassistant/components/frontend/www_static/webcomponents.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -6,6 +6,7 @@ Provides functionality to group devices that can be turned on or off.
|
||||
"""
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
|
||||
@ -103,9 +104,7 @@ class Group(object):
|
||||
self.name = name
|
||||
self.user_defined = user_defined
|
||||
|
||||
self.entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
hass.states.entity_ids(DOMAIN))
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
|
||||
|
||||
self.tracking = []
|
||||
self.group_on, self.group_off = None, None
|
||||
|
128
homeassistant/components/history.py
Normal file
128
homeassistant/components/history.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""
|
||||
homeassistant.components.history
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provide pre-made queries on top of the recorder component.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
import homeassistant.components.recorder as recorder
|
||||
|
||||
DOMAIN = 'history'
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
|
||||
def last_5_states(entity_id):
|
||||
""" Return the last 5 states for entity_id. """
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
query = """
|
||||
SELECT * FROM states WHERE entity_id=? AND
|
||||
last_changed=last_updated
|
||||
ORDER BY last_changed DESC LIMIT 0, 5
|
||||
"""
|
||||
|
||||
return recorder.query_states(query, (entity_id, ))
|
||||
|
||||
|
||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
"""
|
||||
Return states changes during period start_time - end_time.
|
||||
"""
|
||||
where = "last_changed=last_updated AND last_changed > ? "
|
||||
data = [start_time]
|
||||
|
||||
if end_time is not None:
|
||||
where += "AND last_changed < ? "
|
||||
data.append(end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
where += "AND entity_id = ? "
|
||||
data.append(entity_id.lower())
|
||||
|
||||
query = ("SELECT * FROM states WHERE {} "
|
||||
"ORDER BY entity_id, last_changed ASC").format(where)
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
|
||||
result = defaultdict(list)
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time):
|
||||
state.last_changed = start_time
|
||||
result[state.entity_id].append(state)
|
||||
|
||||
# Append all changes to it
|
||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||
result[entity_id].extend(group)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_states(point_in_time, entity_ids=None, run=None):
|
||||
""" Returns the states at a specific point in time. """
|
||||
if run is None:
|
||||
run = recorder.run_information(point_in_time)
|
||||
|
||||
where = run.where_after_start_run + "AND created < ? "
|
||||
where_data = [point_in_time]
|
||||
|
||||
if entity_ids is not None:
|
||||
where += "AND entity_id IN ({}) ".format(
|
||||
",".join(['?'] * len(entity_ids)))
|
||||
where_data.extend(entity_ids)
|
||||
|
||||
query = """
|
||||
SELECT * FROM states
|
||||
INNER JOIN (
|
||||
SELECT max(state_id) AS max_state_id
|
||||
FROM states WHERE {}
|
||||
GROUP BY entity_id)
|
||||
WHERE state_id = max_state_id
|
||||
""".format(where)
|
||||
|
||||
return recorder.query_states(query, where_data)
|
||||
|
||||
|
||||
def get_state(point_in_time, entity_id, run=None):
|
||||
""" Return a state at a specific point in time. """
|
||||
states = get_states(point_in_time, (entity_id,), run)
|
||||
|
||||
return states[0] if states else None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup history hooks. """
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(
|
||||
r'/api/history/entity/(?P<entity_id>[a-zA-Z\._0-9]+)/'
|
||||
r'recent_states'),
|
||||
_api_last_5_states)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/api/history/period'), _api_history_period)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _api_last_5_states(handler, path_match, data):
|
||||
""" Return the last 5 states for an entity id as JSON. """
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
handler.write_json(last_5_states(entity_id))
|
||||
|
||||
|
||||
def _api_history_period(handler, path_match, data):
|
||||
""" Return history over a period of time. """
|
||||
# 1 day for now..
|
||||
start_time = datetime.now() - timedelta(seconds=86400)
|
||||
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
state_changes_during_period(start_time, entity_id=entity_id).values())
|
@ -74,22 +74,18 @@ Example result:
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
import gzip
|
||||
import os
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
import homeassistant as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
|
||||
from homeassistant.helpers import TrackStates
|
||||
from homeassistant.const import SERVER_PORT, AUTH_HEADER
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
from . import frontend
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
|
||||
DOMAIN = "http"
|
||||
DEPENDENCIES = []
|
||||
@ -146,6 +142,7 @@ def setup(hass, config=None):
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True).start())
|
||||
|
||||
hass.http = server
|
||||
hass.local_api = rem.API(util.get_local_ip(), api_password, server_port)
|
||||
|
||||
return True
|
||||
@ -168,12 +165,13 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
self.api_password = api_password
|
||||
self.development = development
|
||||
self.no_password_set = no_password_set
|
||||
self.paths = []
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
|
||||
if development:
|
||||
_LOGGER.info("running frontend in development mode")
|
||||
_LOGGER.info("running http in development mode")
|
||||
|
||||
def start(self):
|
||||
""" Starts the server. """
|
||||
@ -184,10 +182,19 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
_LOGGER.info(
|
||||
"Starting web interface at http://%s:%d", *self.server_address)
|
||||
|
||||
# 31-1-2015: Refactored frontend/api components out of this component
|
||||
# To prevent stuff from breaking, load the two extracted components
|
||||
bootstrap.setup_component(self.hass, 'api')
|
||||
bootstrap.setup_component(self.hass, 'frontend')
|
||||
|
||||
self.serve_forever()
|
||||
|
||||
def register_path(self, method, url, callback, require_auth=True):
|
||||
""" Regitsters a path wit the server. """
|
||||
self.paths.append((method, url, callback, require_auth))
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# pylint: disable=too-many-public-methods,too-many-locals
|
||||
class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"""
|
||||
Handles incoming HTTP requests
|
||||
@ -198,59 +205,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
server_version = "HomeAssistant/1.0"
|
||||
|
||||
PATHS = [ # debug interface
|
||||
('GET', URL_ROOT, '_handle_get_root'),
|
||||
|
||||
# /api - for validation purposes
|
||||
('GET', URL_API, '_handle_get_api'),
|
||||
|
||||
# /states
|
||||
('GET', URL_API_STATES, '_handle_get_api_states'),
|
||||
('GET',
|
||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_get_api_states_entity'),
|
||||
('POST',
|
||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_post_state_entity'),
|
||||
('PUT',
|
||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_post_state_entity'),
|
||||
|
||||
# /events
|
||||
('GET', URL_API_EVENTS, '_handle_get_api_events'),
|
||||
('POST',
|
||||
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_api_post_events_event'),
|
||||
|
||||
# /services
|
||||
('GET', URL_API_SERVICES, '_handle_get_api_services'),
|
||||
('POST',
|
||||
re.compile((r'/api/services/'
|
||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
'_handle_post_api_services_domain_service'),
|
||||
|
||||
# /event_forwarding
|
||||
('POST', URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
|
||||
('DELETE', URL_API_EVENT_FORWARD,
|
||||
'_handle_delete_api_event_forward'),
|
||||
|
||||
# Static files
|
||||
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
'_handle_get_static'),
|
||||
('HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
'_handle_get_static')
|
||||
]
|
||||
|
||||
use_json = False
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
url = urlparse(self.path)
|
||||
|
||||
if url.path.startswith('/api/'):
|
||||
self.use_json = True
|
||||
|
||||
# Read query input
|
||||
data = parse_qs(url.query)
|
||||
|
||||
@ -267,12 +225,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
try:
|
||||
data.update(json.loads(body_content))
|
||||
except (TypeError, ValueError):
|
||||
# TypeError is JSON object is not a dict
|
||||
# TypeError if JSON object is not a dict
|
||||
# ValueError if we could not parse JSON
|
||||
_LOGGER.exception("Exception parsing JSON: %s",
|
||||
body_content)
|
||||
|
||||
self._json_message(
|
||||
_LOGGER.exception(
|
||||
"Exception parsing JSON: %s", body_content)
|
||||
self.write_json_message(
|
||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
@ -293,10 +250,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
# Var to hold the handler for this path and method if found
|
||||
handle_request_method = False
|
||||
require_auth = True
|
||||
|
||||
# Check every handler to find matching result
|
||||
for t_method, t_path, t_handler in RequestHandler.PATHS:
|
||||
|
||||
for t_method, t_path, t_handler, t_auth in self.server.paths:
|
||||
# we either do string-comparison or regular expression matching
|
||||
# pylint: disable=maybe-no-member
|
||||
if isinstance(t_path, str):
|
||||
@ -306,7 +263,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
if path_match and method == t_method:
|
||||
# Call the method
|
||||
handle_request_method = getattr(self, t_handler)
|
||||
handle_request_method = t_handler
|
||||
require_auth = t_auth
|
||||
break
|
||||
|
||||
elif path_match:
|
||||
@ -315,13 +273,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
# Did we find a handler for the incoming request?
|
||||
if handle_request_method:
|
||||
|
||||
# For API calls we need a valid password
|
||||
if self.use_json and api_password != self.server.api_password:
|
||||
self._json_message(
|
||||
# For some calls we need a valid password
|
||||
if require_auth and api_password != self.server.api_password:
|
||||
self.write_json_message(
|
||||
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
|
||||
|
||||
else:
|
||||
handle_request_method(path_match, data)
|
||||
handle_request_method(self, path_match, data)
|
||||
|
||||
elif path_matched_but_not_method:
|
||||
self.send_response(HTTP_METHOD_NOT_ALLOWED)
|
||||
@ -351,275 +309,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
""" DELETE request handler. """
|
||||
self._handle_request('DELETE')
|
||||
|
||||
def _handle_get_root(self, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
|
||||
write = lambda txt: self.wfile.write((txt + "\n").encode("UTF-8"))
|
||||
|
||||
self.send_response(HTTP_OK)
|
||||
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
|
||||
if self.server.development:
|
||||
app_url = "polymer/splash-login.html"
|
||||
else:
|
||||
app_url = "frontend-{}.html".format(frontend.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = (self.server.api_password if self.server.no_password_set
|
||||
else data.get('api_password', ''))
|
||||
|
||||
write(("<!doctype html>"
|
||||
"<html>"
|
||||
"<head><title>Home Assistant</title>"
|
||||
"<meta name='mobile-web-app-capable' content='yes'>"
|
||||
"<link rel='shortcut icon' href='/static/favicon.ico' />"
|
||||
"<link rel='icon' type='image/png' "
|
||||
" href='/static/favicon-192x192.png' sizes='192x192'>"
|
||||
"<meta name='viewport' content='width=device-width, "
|
||||
" user-scalable=no, initial-scale=1.0, "
|
||||
" minimum-scale=1.0, maximum-scale=1.0' />"
|
||||
"<meta name='theme-color' content='#03a9f4'>"
|
||||
"</head>"
|
||||
"<body fullbleed>"
|
||||
"<h3 id='init' align='center'>Initializing Home Assistant</h3>"
|
||||
"<script"
|
||||
" src='/static/webcomponents.min.js'></script>"
|
||||
"<link rel='import' href='/static/{}' />"
|
||||
"<splash-login auth='{}'></splash-login>"
|
||||
"</body></html>").format(app_url, auth))
|
||||
|
||||
def _handle_get_api(self, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
self._json_message("API running.")
|
||||
|
||||
def _handle_get_api_states(self, path_match, data):
|
||||
""" Returns a dict containing all entity ids and their state. """
|
||||
self._write_json(self.server.hass.states.all())
|
||||
|
||||
def _handle_get_api_states_entity(self, path_match, data):
|
||||
""" Returns the state of a specific entity. """
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
state = self.server.hass.states.get(entity_id)
|
||||
|
||||
if state:
|
||||
self._write_json(state)
|
||||
else:
|
||||
self._json_message("State does not exist.", HTTP_NOT_FOUND)
|
||||
|
||||
def _handle_post_state_entity(self, path_match, data):
|
||||
""" Handles updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
try:
|
||||
new_state = data['state']
|
||||
except KeyError:
|
||||
self._json_message("state not specified", HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
attributes = data['attributes'] if 'attributes' in data else None
|
||||
|
||||
is_new_state = self.server.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.server.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
state = self.server.hass.states.get(entity_id)
|
||||
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
|
||||
self._write_json(
|
||||
state.as_dict(),
|
||||
status_code=status_code,
|
||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
def _handle_get_api_events(self, path_match, data):
|
||||
""" Handles getting overview of event listeners. """
|
||||
self._write_json([{"event": key, "listener_count": value}
|
||||
for key, value
|
||||
in self.server.hass.bus.listeners.items()])
|
||||
|
||||
def _handle_api_post_events_event(self, path_match, event_data):
|
||||
""" Handles firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/api/events/<event_type>
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
event_type = path_match.group('event_type')
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
self._json_message("event_data should be an object",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
|
||||
self._json_message("Event {} fired.".format(event_type))
|
||||
|
||||
def _handle_get_api_services(self, path_match, data):
|
||||
""" Handles getting overview of services. """
|
||||
self._write_json(
|
||||
[{"domain": key, "services": value}
|
||||
for key, value
|
||||
in self.server.hass.services.services.items()])
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(self, path_match, data):
|
||||
""" Handles calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
|
||||
with TrackStates(self.server.hass) as changed_states:
|
||||
self.server.hass.services.call(domain, service, data, True)
|
||||
|
||||
self._write_json(changed_states)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(self, path_match, data):
|
||||
""" Handles adding an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
self._json_message("No host or api_password received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
self._json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
self._json_message(
|
||||
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if self.server.event_forwarder is None:
|
||||
self.server.event_forwarder = \
|
||||
rem.EventForwarder(self.server.hass)
|
||||
|
||||
self.server.event_forwarder.connect(api)
|
||||
|
||||
self._json_message("Event forwarding setup.")
|
||||
|
||||
def _handle_delete_api_event_forward(self, path_match, data):
|
||||
""" Handles deleting an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
self._json_message("No host received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
self._json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if self.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.server.event_forwarder.disconnect(api)
|
||||
|
||||
self._json_message("Event forwarding cancelled.")
|
||||
|
||||
def _handle_get_static(self, path_match, data):
|
||||
""" Returns a static file. """
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
# Strip md5 hash out of frontend filename
|
||||
if re.match(r'^frontend-[A-Za-z0-9]{32}\.html$', req_file):
|
||||
req_file = "frontend.html"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
inp = None
|
||||
|
||||
try:
|
||||
inp = open(path, 'rb')
|
||||
|
||||
do_gzip = 'gzip' in self.headers.get('accept-encoding', '')
|
||||
|
||||
self.send_response(HTTP_OK)
|
||||
|
||||
ctype = self.guess_type(path)
|
||||
self.send_header("Content-Type", ctype)
|
||||
|
||||
# Add cache if not development
|
||||
if not self.server.development:
|
||||
# 1 year in seconds
|
||||
cache_time = 365 * 86400
|
||||
|
||||
self.send_header(
|
||||
"Cache-Control", "public, max-age={}".format(cache_time))
|
||||
self.send_header(
|
||||
"Expires", self.date_time_string(time.time()+cache_time))
|
||||
|
||||
if do_gzip:
|
||||
gzip_data = gzip.compress(inp.read())
|
||||
|
||||
self.send_header("Content-Encoding", "gzip")
|
||||
self.send_header("Vary", "Accept-Encoding")
|
||||
self.send_header("Content-Length", str(len(gzip_data)))
|
||||
|
||||
else:
|
||||
fs = os.fstat(inp.fileno())
|
||||
self.send_header("Content-Length", str(fs[6]))
|
||||
|
||||
self.end_headers()
|
||||
|
||||
if self.command == 'HEAD':
|
||||
return
|
||||
|
||||
elif do_gzip:
|
||||
self.wfile.write(gzip_data)
|
||||
|
||||
else:
|
||||
self.copyfile(inp, self.wfile)
|
||||
|
||||
except IOError:
|
||||
self.send_response(HTTP_NOT_FOUND)
|
||||
self.end_headers()
|
||||
|
||||
finally:
|
||||
if inp:
|
||||
inp.close()
|
||||
|
||||
def _json_message(self, message, status_code=HTTP_OK):
|
||||
def write_json_message(self, message, status_code=HTTP_OK):
|
||||
""" Helper method to return a message to the caller. """
|
||||
self._write_json({'message': message}, status_code=status_code)
|
||||
self.write_json({'message': message}, status_code=status_code)
|
||||
|
||||
def _write_json(self, data=None, status_code=HTTP_OK, location=None):
|
||||
def write_json(self, data=None, status_code=HTTP_OK, location=None):
|
||||
""" Helper method to return JSON to the caller. """
|
||||
self.send_response(status_code)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
@ -633,3 +327,56 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
self.wfile.write(
|
||||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
||||
def write_file(self, path):
|
||||
""" Returns a file to the user. """
|
||||
try:
|
||||
with open(path, 'rb') as inp:
|
||||
self.write_file_pointer(self.guess_type(path), inp)
|
||||
|
||||
except IOError:
|
||||
self.send_response(HTTP_NOT_FOUND)
|
||||
self.end_headers()
|
||||
_LOGGER.exception("Unable to serve %s", path)
|
||||
|
||||
def write_file_pointer(self, content_type, inp):
|
||||
"""
|
||||
Helper function to write a file pointer to the user.
|
||||
Does not do error handling.
|
||||
"""
|
||||
do_gzip = 'gzip' in self.headers.get('accept-encoding', '')
|
||||
|
||||
self.send_response(HTTP_OK)
|
||||
self.send_header("Content-Type", content_type)
|
||||
|
||||
# Add cache if not development
|
||||
if not self.server.development:
|
||||
# 1 year in seconds
|
||||
cache_time = 365 * 86400
|
||||
|
||||
self.send_header(
|
||||
"Cache-Control", "public, max-age={}".format(cache_time))
|
||||
self.send_header(
|
||||
"Expires", self.date_time_string(time.time()+cache_time))
|
||||
|
||||
if do_gzip:
|
||||
gzip_data = gzip.compress(inp.read())
|
||||
|
||||
self.send_header("Content-Encoding", "gzip")
|
||||
self.send_header("Vary", "Accept-Encoding")
|
||||
self.send_header("Content-Length", str(len(gzip_data)))
|
||||
|
||||
else:
|
||||
fst = os.fstat(inp.fileno())
|
||||
self.send_header("Content-Length", str(fst[6]))
|
||||
|
||||
self.end_headers()
|
||||
|
||||
if self.command == 'HEAD':
|
||||
return
|
||||
|
||||
elif do_gzip:
|
||||
self.wfile.write(gzip_data)
|
||||
|
||||
else:
|
||||
self.copyfile(inp, self.wfile)
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"version": "0.1.0",
|
||||
"authors": [
|
||||
"Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
],
|
||||
"main": "splash-login.html",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
],
|
||||
"dependencies": {
|
||||
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.2",
|
||||
"font-roboto": "Polymer/font-roboto#~0.5.2",
|
||||
"core-header-panel": "Polymer/core-header-panel#~0.5.2",
|
||||
"core-toolbar": "Polymer/core-toolbar#~0.5.2",
|
||||
"core-tooltip": "Polymer/core-tooltip#~0.5.2",
|
||||
"core-menu": "Polymer/core-menu#~0.5.2",
|
||||
"core-item": "Polymer/core-item#~0.5.2",
|
||||
"core-input": "Polymer/core-input#~0.5.2",
|
||||
"core-icons": "polymer/core-icons#~0.5.2",
|
||||
"core-image": "polymer/core-image#~0.5.2",
|
||||
"paper-toast": "Polymer/paper-toast#~0.5.2",
|
||||
"paper-dialog": "Polymer/paper-dialog#~0.5.2",
|
||||
"paper-spinner": "Polymer/paper-spinner#~0.5.2",
|
||||
"paper-button": "Polymer/paper-button#~0.5.2",
|
||||
"paper-input": "Polymer/paper-input#~0.5.2",
|
||||
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.2",
|
||||
"paper-tabs": "polymer/paper-tabs#~0.5.2",
|
||||
"paper-icon-button": "polymer/paper-icon-button#~0.5.2",
|
||||
"paper-menu-button": "polymer/paper-menu-button#~0.5.2",
|
||||
"paper-dropdown": "polymer/paper-dropdown#~0.5.2",
|
||||
"paper-item": "polymer/paper-item#~0.5.2",
|
||||
"moment": "~2.8.4",
|
||||
"core-style": "polymer/core-style#~0.5.2",
|
||||
"paper-slider": "polymer/paper-slider#~0.5.2",
|
||||
"color-picker-element": "~0.0.2"
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-display.html">
|
||||
<link rel="import" href="state-card-toggle.html">
|
||||
<link rel="import" href="state-card-thermostat.html">
|
||||
<link rel="import" href="state-card-configurator.html">
|
||||
|
||||
<polymer-element name="state-card-content" attributes="api stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='card'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObjChanged: function() {
|
||||
while (this.$.card.lastChild) {
|
||||
this.$.card.removeChild(this.$.card.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + this.stateObj.cardType);
|
||||
stateCard.api = this.api;
|
||||
stateCard.stateObj = this.stateObj;
|
||||
this.$.card.appendChild(stateCard);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,89 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/events-list.html">
|
||||
|
||||
<polymer-element name="event-fire-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Fire Event"
|
||||
class='two-column'
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input
|
||||
id="inputType" label="Event Type" floatingLabel="true"
|
||||
autofocus required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Event Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available events:</b>
|
||||
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.eventSelected = this.eventSelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(eventType, eventData) {
|
||||
this.setEventType(eventType);
|
||||
this.setEventData(eventData);
|
||||
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setEventType: function(eventType) {
|
||||
this.$.inputType.value = eventType;
|
||||
},
|
||||
|
||||
setEventData: function(eventData) {
|
||||
this.$.inputData.value = eventData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
},
|
||||
|
||||
eventSelected: function(eventType) {
|
||||
this.setEventType(eventType);
|
||||
},
|
||||
|
||||
clickFireEvent: function() {
|
||||
try {
|
||||
this.api.fire_event(
|
||||
this.$.inputType.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
this.$.dialog.close();
|
||||
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,87 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/services-list.html">
|
||||
|
||||
<polymer-element name="service-call-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Call Service"
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Service Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available services:</b>
|
||||
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.serviceSelected = this.serviceSelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(domain, service, serviceData) {
|
||||
this.setService(domain, service);
|
||||
this.$.inputData.value = serviceData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setService: function(domain, service) {
|
||||
this.$.inputDomain.value = domain;
|
||||
this.$.inputService.value = service;
|
||||
},
|
||||
|
||||
serviceSelected: function(domain, service) {
|
||||
this.setService(domain, service);
|
||||
},
|
||||
|
||||
clickCallService: function() {
|
||||
try {
|
||||
this.api.call_service(
|
||||
this.$.inputDomain.value,
|
||||
this.$.inputService.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
|
||||
this.$.dialog.close();
|
||||
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,105 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/entity-list.html">
|
||||
|
||||
<polymer-element name="state-set-dialog" attributes="api">
|
||||
<template>
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Set State"
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
|
||||
<p>
|
||||
This dialog will update the representation of the device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</p>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="State attributes (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Current entities:</b>
|
||||
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.entitySelected = this.entitySelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(entityId, state, stateData) {
|
||||
this.setEntityId(entityId);
|
||||
this.setState(state);
|
||||
this.setStateData(stateData);
|
||||
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setEntityId: function(entityId) {
|
||||
this.$.inputEntityID.value = entityId;
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.$.inputState.value = state;
|
||||
},
|
||||
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
this.$.inputData.value = value;
|
||||
},
|
||||
|
||||
entitySelected: function(entityId) {
|
||||
this.setEntityId(entityId);
|
||||
|
||||
var state = this.api.getState(entityId);
|
||||
this.setState(state.state);
|
||||
this.setStateData(state.attributes);
|
||||
},
|
||||
|
||||
clickSetState: function(ev) {
|
||||
try {
|
||||
this.api.set_state(
|
||||
this.$.inputEntityID.value,
|
||||
this.$.inputState.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
|
||||
);
|
||||
|
||||
this.$.dialog.close();
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,477 +0,0 @@
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<link rel="import" href="dialogs/event-fire-dialog.html">
|
||||
<link rel="import" href="dialogs/service-call-dialog.html">
|
||||
<link rel="import" href="dialogs/state-set-dialog.html">
|
||||
<link rel="import" href="dialogs/more-info-dialog.html">
|
||||
|
||||
<script>
|
||||
var ha = {};
|
||||
ha.util = {};
|
||||
|
||||
ha.util.parseTime = function(timeString) {
|
||||
return moment(timeString, "HH:mm:ss DD-MM-YYYY");
|
||||
};
|
||||
|
||||
ha.util.relativeTime = function(timeString) {
|
||||
return ha.util.parseTime(timeString).fromNow();
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.relativeHATime = function(timeString) {
|
||||
return ha.util.relativeTime(timeString);
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
|
||||
return (timeString || "").split(' ')[0];
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<polymer-element name="home-assistant-api" attributes="auth">
|
||||
<template>
|
||||
<paper-toast id="toast" role="alert" text=""></paper-toast>
|
||||
<event-fire-dialog id="eventDialog" api={{api}}></event-fire-dialog>
|
||||
<service-call-dialog id="serviceDialog" api={{api}}></service-call-dialog>
|
||||
<state-set-dialog id="stateSetDialog" api={{api}}></state-set-dialog>
|
||||
<more-info-dialog id="moreInfoDialog" api={{api}}></more-info-dialog>
|
||||
</template>
|
||||
<script>
|
||||
var domainsWithCard = ['thermostat', 'configurator'];
|
||||
var domainsWithMoreInfo = ['light', 'group', 'sun', 'configurator'];
|
||||
|
||||
State = function(json, api) {
|
||||
this.api = api;
|
||||
|
||||
this.attributes = json.attributes;
|
||||
|
||||
this.entity_id = json.entity_id;
|
||||
var parts = json.entity_id.split(".");
|
||||
this.domain = parts[0];
|
||||
this.object_id = parts[1];
|
||||
|
||||
if(this.attributes.friendly_name) {
|
||||
this.entityDisplay = this.attributes.friendly_name;
|
||||
} else {
|
||||
this.entityDisplay = this.object_id.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
this.state = json.state;
|
||||
this.last_changed = json.last_changed;
|
||||
};
|
||||
|
||||
Object.defineProperties(State.prototype, {
|
||||
stateDisplay: {
|
||||
get: function() {
|
||||
var state = this.state.replace(/_/g, " ");
|
||||
if(this.attributes.unit_of_measurement) {
|
||||
return state + " " + this.attributes.unit_of_measurement;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isCustomGroup: {
|
||||
get: function() {
|
||||
return this.domain == "group" && !this.attributes.auto;
|
||||
}
|
||||
},
|
||||
|
||||
canToggle: {
|
||||
get: function() {
|
||||
// groups that have the on/off state or if there is a turn_on service
|
||||
return ((this.domain == 'group' &&
|
||||
(this.state == 'on' || this.state == 'off')) ||
|
||||
this.api.hasService(this.domain, 'turn_on'));
|
||||
}
|
||||
},
|
||||
|
||||
// how to render the card for this state
|
||||
cardType: {
|
||||
get: function() {
|
||||
if(domainsWithCard.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else if(this.canToggle) {
|
||||
return "toggle";
|
||||
} else {
|
||||
return "display";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// how to render the more info of this state
|
||||
moreInfoType: {
|
||||
get: function() {
|
||||
if(domainsWithMoreInfo.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
relativeLastChanged: {
|
||||
get: function() {
|
||||
return ha.util.relativeTime(this.last_changed);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Polymer({
|
||||
auth: "not-set",
|
||||
states: [],
|
||||
services: [],
|
||||
events: [],
|
||||
stateUpdateTimeout: null,
|
||||
|
||||
created: function() {
|
||||
this.api = this;
|
||||
|
||||
// so we can pass these methods safely as callbacks
|
||||
this.turn_on = this.turn_on.bind(this);
|
||||
this.turn_off = this.turn_off.bind(this);
|
||||
},
|
||||
|
||||
// local methods
|
||||
removeState: function(entityId) {
|
||||
var state = this.getState(entityId);
|
||||
|
||||
if (state !== null) {
|
||||
this.states.splice(this.states.indexOf(state), 1);
|
||||
}
|
||||
},
|
||||
|
||||
getState: function(entityId) {
|
||||
var found = this.states.filter(function(state) {
|
||||
return state.entity_id == entityId;
|
||||
}, this);
|
||||
|
||||
return found.length > 0 ? found[0] : null;
|
||||
},
|
||||
|
||||
getStates: function(entityIds) {
|
||||
var states = [];
|
||||
var state;
|
||||
for(var i = 0; i < entityIds.length; i++) {
|
||||
state = this.getState(entityIds[i]);
|
||||
|
||||
if(state !== null) {
|
||||
states.push(state);
|
||||
}
|
||||
}
|
||||
return states;
|
||||
},
|
||||
|
||||
getEntityIDs: function() {
|
||||
return this.states.map(
|
||||
function(state) { return state.entity_id; });
|
||||
},
|
||||
|
||||
hasService: function(domain, service) {
|
||||
var found = this.services.filter(function(serv) {
|
||||
return serv.domain == domain && serv.services.indexOf(service) !== -1;
|
||||
}, this);
|
||||
|
||||
return found.length > 0;
|
||||
},
|
||||
|
||||
getCustomGroups: function() {
|
||||
return this.states.filter(function(state) { return state.isCustomGroup;});
|
||||
},
|
||||
|
||||
_laterFetchStates: function() {
|
||||
if(this.stateUpdateTimeout) {
|
||||
clearTimeout(this.stateUpdateTimeout);
|
||||
}
|
||||
|
||||
// update states in 60 seconds
|
||||
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
|
||||
},
|
||||
|
||||
_sortStates: function() {
|
||||
this.states.sort(function(one, two) {
|
||||
if (one.entity_id > two.entity_id) {
|
||||
return 1;
|
||||
} else if (one.entity_id < two.entity_id) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Pushes a new state to the state machine.
|
||||
* Will resort the states after a push and fire states-updated event.
|
||||
*/
|
||||
_pushNewState: function(new_state) {
|
||||
if (this.__pushNewState(new_state)) {
|
||||
this._sortStates();
|
||||
}
|
||||
|
||||
this.fire('states-updated');
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates or updates a state. Returns if a new state was added.
|
||||
*/
|
||||
__pushNewState: function(new_state) {
|
||||
var curState = this.getState(new_state.entity_id);
|
||||
|
||||
if (curState === null) {
|
||||
this.states.push(new State(new_state, this));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
curState.attributes = new_state.attributes;
|
||||
curState.last_changed = new_state.last_changed;
|
||||
curState.state = new_state.state;
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_pushNewStates: function(newStates, removeNonPresent) {
|
||||
removeNonPresent = !!removeNonPresent;
|
||||
var currentEntityIds = removeNonPresent ? this.getEntityIDs() : [];
|
||||
|
||||
var hasNew = newStates.reduce(function(hasNew, newState) {
|
||||
var isNewState = this.__pushNewState(newState);
|
||||
|
||||
if (isNewState) {
|
||||
return true;
|
||||
} else if(removeNonPresent) {
|
||||
currentEntityIds.splice(currentEntityIds.indexOf(newState.entity_id), 1);
|
||||
}
|
||||
|
||||
return hasNew;
|
||||
}.bind(this), false);
|
||||
|
||||
currentEntityIds.forEach(function(entityId) {
|
||||
this.removeState(entityId);
|
||||
}.bind(this));
|
||||
|
||||
if (hasNew) {
|
||||
this._sortStates();
|
||||
}
|
||||
|
||||
this.fire('states-updated');
|
||||
},
|
||||
|
||||
// call api methods
|
||||
fetchAll: function() {
|
||||
this.fetchStates();
|
||||
this.fetchServices();
|
||||
this.fetchEvents();
|
||||
},
|
||||
|
||||
fetchState: function(entityId) {
|
||||
var successStateUpdate = function(new_state) {
|
||||
this._pushNewState(new_state);
|
||||
};
|
||||
|
||||
this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this));
|
||||
},
|
||||
|
||||
fetchStates: function(onSuccess, onError) {
|
||||
var successStatesUpdate = function(newStates) {
|
||||
this._pushNewStates(newStates, true);
|
||||
|
||||
this._laterFetchStates();
|
||||
|
||||
if(onSuccess) {
|
||||
onSuccess(this.states);
|
||||
}
|
||||
};
|
||||
|
||||
this.call_api(
|
||||
"GET", "states", null, successStatesUpdate.bind(this), onError);
|
||||
},
|
||||
|
||||
fetchEvents: function(onSuccess, onError) {
|
||||
var successEventsUpdated = function(events) {
|
||||
this.events = events;
|
||||
|
||||
this.fire('events-updated');
|
||||
|
||||
if(onSuccess) {
|
||||
onSuccess(events);
|
||||
}
|
||||
};
|
||||
|
||||
this.call_api(
|
||||
"GET", "events", null, successEventsUpdated.bind(this), onError);
|
||||
},
|
||||
|
||||
fetchServices: function(onSuccess, onError) {
|
||||
var successServicesUpdated = function(services) {
|
||||
this.services = services;
|
||||
|
||||
this.fire('services-updated');
|
||||
|
||||
if(onSuccess) {
|
||||
onSuccess(this.services);
|
||||
}
|
||||
};
|
||||
|
||||
this.call_api(
|
||||
"GET", "services", null, successServicesUpdated.bind(this), onError);
|
||||
},
|
||||
|
||||
turn_on: function(entity_id, options) {
|
||||
this.call_service(
|
||||
"homeassistant", "turn_on", {entity_id: entity_id}, options);
|
||||
},
|
||||
|
||||
turn_off: function(entity_id, options) {
|
||||
this.call_service(
|
||||
"homeassistant", "turn_off", {entity_id: entity_id}, options);
|
||||
},
|
||||
|
||||
set_state: function(entity_id, state, attributes) {
|
||||
var payload = {state: state};
|
||||
|
||||
if(attributes) {
|
||||
payload.attributes = attributes;
|
||||
}
|
||||
|
||||
var successToast = function(new_state) {
|
||||
this.showToast("State of "+entity_id+" set to "+state+".");
|
||||
this._pushNewState(new_state);
|
||||
};
|
||||
|
||||
this.call_api("POST", "states/" + entity_id,
|
||||
payload, successToast.bind(this));
|
||||
},
|
||||
|
||||
call_service: function(domain, service, parameters, options) {
|
||||
parameters = parameters || {};
|
||||
options = options || {};
|
||||
|
||||
var successHandler = function(changed_states) {
|
||||
if(service == "turn_on" && parameters.entity_id) {
|
||||
this.showToast("Turned on " + parameters.entity_id + '.');
|
||||
} else if(service == "turn_off" && parameters.entity_id) {
|
||||
this.showToast("Turned off " + parameters.entity_id + '.');
|
||||
} else {
|
||||
this.showToast("Service "+domain+"/"+service+" called.");
|
||||
}
|
||||
|
||||
this._pushNewStates(changed_states);
|
||||
|
||||
if(options.success) {
|
||||
options.success();
|
||||
}
|
||||
};
|
||||
|
||||
var errorHandler = function(error_data) {
|
||||
if(options.error) {
|
||||
options.error(error_data);
|
||||
}
|
||||
};
|
||||
|
||||
this.call_api("POST", "services/" + domain + "/" + service,
|
||||
parameters, successHandler.bind(this), errorHandler);
|
||||
},
|
||||
|
||||
fire_event: function(eventType, eventData) {
|
||||
eventData = eventData || {};
|
||||
|
||||
var successToast = function() {
|
||||
this.showToast("Event "+eventType+" fired.");
|
||||
};
|
||||
|
||||
this.call_api("POST", "events/" + eventType,
|
||||
eventData, successToast.bind(this));
|
||||
},
|
||||
|
||||
call_api: function(method, path, parameters, onSuccess, onError) {
|
||||
var url = "/api/" + path;
|
||||
|
||||
// set to true to generate a frontend to be used as demo on the website
|
||||
if (false) {
|
||||
if (path === "states" || path === "services" || path === "events") {
|
||||
url = "/demo/" + path + ".json";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var req = new XMLHttpRequest();
|
||||
req.open(method, url, true);
|
||||
req.setRequestHeader("X-HA-access", this.auth);
|
||||
|
||||
req.onreadystatechange = function() {
|
||||
|
||||
if(req.readyState == 4) {
|
||||
if(req.status > 199 && req.status < 300) {
|
||||
if(onSuccess) {
|
||||
onSuccess(JSON.parse(req.responseText));
|
||||
}
|
||||
} else {
|
||||
if(onError) {
|
||||
var data = req.responseText ? JSON.parse(req.responseText) : {};
|
||||
onError(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
if(parameters) {
|
||||
req.send(JSON.stringify(parameters));
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
},
|
||||
|
||||
// show dialogs
|
||||
showmoreInfoDialog: function(entityId) {
|
||||
this.$.moreInfoDialog.show(this.getState(entityId));
|
||||
},
|
||||
|
||||
showEditStateDialog: function(entityId) {
|
||||
var state = this.getState(entityId);
|
||||
|
||||
this.showSetStateDialog(entityId, state.state, state.attributes);
|
||||
},
|
||||
|
||||
showSetStateDialog: function(entityId, state, stateAttributes) {
|
||||
entityId = entityId || "";
|
||||
state = state || "";
|
||||
stateAttributes = stateAttributes || null;
|
||||
|
||||
this.$.stateSetDialog.show(entityId, state, stateAttributes);
|
||||
},
|
||||
|
||||
showFireEventDialog: function(eventType, eventData) {
|
||||
eventType = eventType || "";
|
||||
eventData = eventData || "";
|
||||
|
||||
this.$.eventDialog.show(eventType, eventData);
|
||||
},
|
||||
|
||||
showCallServiceDialog: function(domain, service, serviceData) {
|
||||
domain = domain || "";
|
||||
service = service || "";
|
||||
serviceData = serviceData || "";
|
||||
|
||||
this.$.serviceDialog.show(domain, service, serviceData);
|
||||
},
|
||||
|
||||
showToast: function(message) {
|
||||
this.$.toast.text = message;
|
||||
this.$.toast.show();
|
||||
},
|
||||
|
||||
logOut: function() {
|
||||
this.auth = "";
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,172 +0,0 @@
|
||||
<link rel="import" href="bower_components/core-header-panel/core-header-panel.html">
|
||||
<link rel="import" href="bower_components/core-toolbar/core-toolbar.html">
|
||||
<link rel="import" href="bower_components/paper-tabs/paper-tabs.html">
|
||||
<link rel="import" href="bower_components/paper-tabs/paper-tab.html">
|
||||
<link rel="import" href="bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel="import" href="bower_components/paper-menu-button/paper-menu-button.html">
|
||||
<link rel="import" href="bower_components/paper-dropdown/paper-dropdown.html">
|
||||
<link rel="import" href="bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="bower_components/paper-item/paper-item.html">
|
||||
|
||||
<link rel="import" href="components/state-cards.html">
|
||||
|
||||
<polymer-element name="home-assistant-main" attributes="api">
|
||||
<template>
|
||||
<style>
|
||||
|
||||
core-header-panel {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background-color: #E5E5E5;
|
||||
}
|
||||
|
||||
core-toolbar {
|
||||
background: #03a9f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
core-toolbar.tall {
|
||||
/* 2x normal height */
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
core-toolbar .bottom {
|
||||
opacity: 0;
|
||||
transition: opacity 0.30s ease-out;
|
||||
}
|
||||
|
||||
core-toolbar.tall .bottom {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
paper-tab {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
paper-menu-button {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
paper-dropdown {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
paper-dropdown .menu {
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
color: black;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
paper-item a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<core-header-panel fit mode="{{hasCustomGroups && 'waterfall-tall'}}">
|
||||
|
||||
<core-toolbar>
|
||||
<div flex>Home Assistant</div>
|
||||
<paper-icon-button icon="refresh"
|
||||
on-click="{{handleRefreshClick}}"></paper-icon-button>
|
||||
<paper-icon-button icon="settings-remote"
|
||||
on-click="{{handleServiceClick}}"></paper-icon-button>
|
||||
|
||||
<paper-menu-button>
|
||||
<paper-icon-button icon="more-vert" noink></paper-icon-button>
|
||||
<paper-dropdown halign="right" duration="200" class="dropdown">
|
||||
<core-menu class="menu">
|
||||
<paper-item>
|
||||
<a on-click={{handleAddStateClick}}>Set State</a>
|
||||
</paper-item>
|
||||
<paper-item>
|
||||
<a on-click={{handleEventClick}}>Trigger Event</a>
|
||||
</paper-item>
|
||||
<paper-item>
|
||||
<a on-click={{handleLogOutClick}}>Log Out</a>
|
||||
</paper-item>
|
||||
</core-menu>
|
||||
</paper-dropdown>
|
||||
</paper-menu-button>
|
||||
|
||||
<template if="{{hasCustomGroups}}">
|
||||
<div class="bottom fit" horizontal layout>
|
||||
<paper-tabs id="tabsHolder" noink flex
|
||||
selected="0" on-core-select="{{tabClicked}}">
|
||||
|
||||
<paper-tab>ALL</paper-tab>
|
||||
<paper-tab data-filter='customgroup'>GROUPS</paper-tab>
|
||||
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</core-toolbar>
|
||||
|
||||
<state-cards
|
||||
api="{{api}}"
|
||||
filter="{{selectedFilter}}"
|
||||
class="content">
|
||||
<h3>Hi there!</h3>
|
||||
<p>
|
||||
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
|
||||
</p>
|
||||
<p>
|
||||
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
|
||||
</p>
|
||||
</state-cards>
|
||||
|
||||
</core-header-panel>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
selectedFilter: null,
|
||||
hasCustomGroups: false,
|
||||
|
||||
observe: {
|
||||
'api.states': 'updateHasCustomGroup'
|
||||
},
|
||||
|
||||
// computed: {
|
||||
// hasCustomGroups: "api.getCustomGroups().length > 0"
|
||||
// },
|
||||
|
||||
tabClicked: function(ev) {
|
||||
if(ev.detail.isSelected) {
|
||||
// will be null for ALL tab
|
||||
this.selectedFilter = ev.detail.item.getAttribute('data-filter');
|
||||
}
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
this.api.fetchAll();
|
||||
},
|
||||
|
||||
handleEventClick: function() {
|
||||
this.api.showFireEventDialog();
|
||||
},
|
||||
|
||||
handleServiceClick: function() {
|
||||
this.api.showCallServiceDialog();
|
||||
},
|
||||
|
||||
handleAddStateClick: function() {
|
||||
this.api.showSetStateDialog();
|
||||
},
|
||||
|
||||
handleLogOutClick: function() {
|
||||
this.api.logOut();
|
||||
},
|
||||
|
||||
updateHasCustomGroup: function() {
|
||||
this.hasCustomGroups = this.api.getCustomGroups().length > 0;
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,45 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="more-info-default.html">
|
||||
<link rel="import" href="more-info-light.html">
|
||||
<link rel="import" href="more-info-group.html">
|
||||
<link rel="import" href="more-info-sun.html">
|
||||
<link rel="import" href="more-info-configurator.html">
|
||||
|
||||
<polymer-element name="more-info-content" attributes="api stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='moreInfo' class='{{classNames}}'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
classNames: '',
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'stateAttributesChanged',
|
||||
},
|
||||
|
||||
stateObjChanged: function() {
|
||||
while (this.$.moreInfo.lastChild) {
|
||||
this.$.moreInfo.removeChild(this.$.moreInfo.lastChild);
|
||||
}
|
||||
|
||||
var moreInfo = document.createElement("more-info-" + this.stateObj.moreInfoType);
|
||||
moreInfo.api = this.api;
|
||||
moreInfo.stateObj = this.stateObj;
|
||||
this.$.moreInfo.appendChild(moreInfo);
|
||||
},
|
||||
|
||||
stateAttributesChanged: function(oldVal, newVal) {
|
||||
this.classNames = Object.keys(newVal).map(
|
||||
function(key) { return "has-" + key; }).join(' ');
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,13 +0,0 @@
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
|
||||
|
||||
<core-iconset-svg id="homeassistant" iconSize="100">
|
||||
<svg><defs>
|
||||
<g id="thermostat">
|
||||
<!--
|
||||
Thermostat icon created by Scott Lewis from the Noun Project
|
||||
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
|
||||
-->
|
||||
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
@ -1,60 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="home-assistant-style" noscript>
|
||||
<template>
|
||||
<core-style id="ha-dialog">
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
|
||||
min-width: 350px;
|
||||
max-width: 700px;
|
||||
|
||||
/* First two are from core-transition-bottom */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
opacity 0.2s ease-in,
|
||||
top .3s,
|
||||
left .3s !important;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
:host.two-column {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 464px) {
|
||||
:host {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
</core-style>
|
||||
</template>
|
||||
</polymer-element>
|
@ -1,156 +0,0 @@
|
||||
<link rel="import" href="bower_components/font-roboto/roboto.html">
|
||||
<link rel="import" href="bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="bower_components/core-input/core-input.html">
|
||||
<link rel="import" href="bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<link rel="import" href="home-assistant-main.html">
|
||||
<link rel="import" href="home-assistant-api.html">
|
||||
<link rel="import" href="resources/home-assistant-style.html">
|
||||
|
||||
<polymer-element name="splash-login" attributes="auth">
|
||||
<template>
|
||||
<style>
|
||||
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login paper-button {
|
||||
margin-left: 242px;
|
||||
}
|
||||
|
||||
.login .interact {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
#validatebox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#validatemessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<home-assistant-style></home-assistant-style>
|
||||
<home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
|
||||
|
||||
<div layout horizontal center fit class='login' id="splash">
|
||||
<div layout vertical center flex>
|
||||
<img src="/static/favicon-192x192.png" />
|
||||
<h1>Home Assistant</h1>
|
||||
<a href="#" id="hideKeyboardOnFocus"></a>
|
||||
<div class='interact' layout vertical>
|
||||
<div id='loginform'>
|
||||
<paper-input-decorator label="Password" id="passwordDecorator">
|
||||
<input is="core-input" type="password" id="passwordInput"
|
||||
value="{{auth}}" on-keyup="{{passwordKeyup}}" autofocus>
|
||||
</paper-input-decorator>
|
||||
<paper-button on-click={{validatePassword}}>Log In</paper-button>
|
||||
</div>
|
||||
|
||||
<div id="validatebox" hidden>
|
||||
<paper-spinner active="true"></paper-spinner><br />
|
||||
<div id="validatemessage">Validating password...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<home-assistant-main api="{{api}}" hidden id="main"></home-assistant-main>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
// can be no_auth, valid_auth
|
||||
state: "no_auth",
|
||||
auth: "",
|
||||
|
||||
ready: function() {
|
||||
this.api = this.$.api;
|
||||
},
|
||||
|
||||
domReady: function() {
|
||||
document.getElementById('init').remove();
|
||||
|
||||
if(this.auth) {
|
||||
this.validatePassword();
|
||||
}
|
||||
},
|
||||
|
||||
authChanged: function(oldVal, newVal) {
|
||||
// log out functionality
|
||||
if(newVal === "" && this.state === "valid_auth") {
|
||||
this.state = "no_auth";
|
||||
}
|
||||
},
|
||||
|
||||
stateChanged: function(oldVal, newVal) {
|
||||
if(newVal === "no_auth") {
|
||||
// set login box showing
|
||||
this.$.loginform.removeAttribute('hidden');
|
||||
this.$.validatebox.setAttribute('hidden', null);
|
||||
|
||||
// reset to initial message
|
||||
this.$.validatemessage.innerHTML = "Validating password...";
|
||||
|
||||
// show splash
|
||||
this.$.splash.removeAttribute('hidden');
|
||||
this.$.main.setAttribute('hidden', null);
|
||||
} else { // valid_auth
|
||||
this.$.splash.setAttribute('hidden', null);
|
||||
this.$.main.removeAttribute('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
passwordKeyup: function(ev) {
|
||||
// validate on enter
|
||||
if(ev.keyCode === 13) {
|
||||
this.validatePassword();
|
||||
|
||||
// clear error after we start typing again
|
||||
} else if(this.$.passwordDecorator.isInvalid) {
|
||||
this.$.passwordDecorator.isInvalid = false;
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: function() {
|
||||
this.$.loginform.setAttribute('hidden', null);
|
||||
this.$.validatebox.removeAttribute('hidden');
|
||||
this.$.hideKeyboardOnFocus.focus();
|
||||
|
||||
var passwordValid = function(result) {
|
||||
this.$.validatemessage.innerHTML = "Loading data...";
|
||||
this.api.fetchEvents();
|
||||
|
||||
this.api.fetchStates(function() {
|
||||
this.state = "valid_auth";
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
var passwordInvalid = function(result) {
|
||||
if(result && result.message) {
|
||||
this.$.passwordDecorator.error = result.message;
|
||||
} else {
|
||||
this.$.passwordDecorator.error = "Unexpected result from API";
|
||||
}
|
||||
this.auth = null;
|
||||
this.$.passwordDecorator.isInvalid = true;
|
||||
this.$.loginform.removeAttribute('hidden');
|
||||
this.$.validatebox.setAttribute('hidden', null);
|
||||
this.$.passwordInput.focus();
|
||||
};
|
||||
|
||||
this.api.fetchServices(passwordValid.bind(this), passwordInvalid.bind(this));
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
File diff suppressed because one or more lines are too long
94
homeassistant/components/light/tellstick.py
Normal file
94
homeassistant/components/light/tellstick.py
Normal file
@ -0,0 +1,94 @@
|
||||
""" Support for Tellstick lights. """
|
||||
import logging
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return tellstick lights. """
|
||||
|
||||
try:
|
||||
import tellcore.telldus as telldus
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception(
|
||||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
switches_and_lights = core.devices()
|
||||
lights = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
lights.append(TellstickLight(switch))
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
||||
class TellstickLight(ToggleDevice):
|
||||
""" Represents a tellstick light """
|
||||
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||
tellcore_constants.TELLSTICK_TURNOFF |
|
||||
tellcore_constants.TELLSTICK_DIM |
|
||||
tellcore_constants.TELLSTICK_UP |
|
||||
tellcore_constants.TELLSTICK_DOWN)
|
||||
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
self.brightness = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self.brightness > 0
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
self.brightness = 0
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
if brightness is None:
|
||||
self.brightness = 255
|
||||
else:
|
||||
self.brightness = brightness
|
||||
|
||||
self.tellstick.dim(self.brightness)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.name
|
||||
}
|
||||
|
||||
attr[ATTR_BRIGHTNESS] = int(self.brightness)
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
if last_command == tellcore_constants.TELLSTICK_TURNON:
|
||||
self.brightness = 255
|
||||
elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
|
||||
self.brightness = 0
|
||||
elif (last_command == tellcore_constants.TELLSTICK_DIM or
|
||||
last_command == tellcore_constants.TELLSTICK_UP or
|
||||
last_command == tellcore_constants.TELLSTICK_DOWN):
|
||||
last_sent_value = self.tellstick.last_sent_value()
|
||||
if last_sent_value is not None:
|
||||
self.brightness = last_sent_value
|
360
homeassistant/components/recorder.py
Normal file
360
homeassistant/components/recorder.py
Normal file
@ -0,0 +1,360 @@
|
||||
"""
|
||||
homeassistant.components.recorder
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Component that records all events and state changes.
|
||||
Allows other components to query this database.
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import queue
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import time
|
||||
import json
|
||||
import atexit
|
||||
|
||||
from homeassistant import Event, EventOrigin, State
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.const import (
|
||||
MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
DOMAIN = "recorder"
|
||||
DEPENDENCIES = []
|
||||
|
||||
DB_FILE = 'home-assistant.db'
|
||||
|
||||
RETURN_ROWCOUNT = "rowcount"
|
||||
RETURN_LASTROWID = "lastrowid"
|
||||
RETURN_ONE_ROW = "one_row"
|
||||
|
||||
_INSTANCE = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query(sql_query, arguments=None):
|
||||
""" Query the database. """
|
||||
_verify_instance()
|
||||
|
||||
return _INSTANCE.query(sql_query, arguments)
|
||||
|
||||
|
||||
def query_states(state_query, arguments=None):
|
||||
""" Query the database and return a list of states. """
|
||||
return [
|
||||
row for row in
|
||||
(row_to_state(row) for row in query(state_query, arguments))
|
||||
if row is not None]
|
||||
|
||||
|
||||
def query_events(event_query, arguments=None):
|
||||
""" Query the database and return a list of states. """
|
||||
return [
|
||||
row for row in
|
||||
(row_to_event(row) for row in query(event_query, arguments))
|
||||
if row is not None]
|
||||
|
||||
|
||||
def row_to_state(row):
|
||||
""" Convert a databsae row to a state. """
|
||||
try:
|
||||
return State(
|
||||
row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4]))
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to state: %s", row)
|
||||
return None
|
||||
|
||||
|
||||
def row_to_event(row):
|
||||
""" Convert a databse row to an event. """
|
||||
try:
|
||||
return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()])
|
||||
except ValueError:
|
||||
# When json.oads fails
|
||||
_LOGGER.exception("Error converting row to event: %s", row)
|
||||
return None
|
||||
|
||||
|
||||
def run_information(point_in_time=None):
|
||||
""" Returns information about current run or the run that
|
||||
covers point_in_time. """
|
||||
_verify_instance()
|
||||
|
||||
if point_in_time is None:
|
||||
return RecorderRun()
|
||||
|
||||
run = _INSTANCE.query(
|
||||
"SELECT * FROM recorder_runs WHERE start>? AND END IS NULL OR END<?",
|
||||
(point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
|
||||
|
||||
return RecorderRun(run) if run else None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the recorder. """
|
||||
# pylint: disable=global-statement
|
||||
global _INSTANCE
|
||||
|
||||
_INSTANCE = Recorder(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RecorderRun(object):
|
||||
""" Represents a recorder run. """
|
||||
def __init__(self, row=None):
|
||||
if row is None:
|
||||
self.start = _INSTANCE.recording_start
|
||||
self.end = None
|
||||
self.closed_incorrect = False
|
||||
else:
|
||||
self.start = datetime.fromtimestamp(row[1])
|
||||
self.end = datetime.fromtimestamp(row[2])
|
||||
self.closed_incorrect = bool(row[3])
|
||||
|
||||
def entity_ids(self, point_in_time=None):
|
||||
"""
|
||||
Return the entity ids that existed in this run.
|
||||
Specify point_in_time if you want to know which existed at that point
|
||||
in time inside the run.
|
||||
"""
|
||||
where = self.where_after_start_run
|
||||
where_data = []
|
||||
|
||||
if point_in_time is not None or self.end is not None:
|
||||
where += "AND created < ? "
|
||||
where_data.append(point_in_time or self.end)
|
||||
|
||||
return [row[0] for row in query(
|
||||
"SELECT entity_id FROM states WHERE {}"
|
||||
"GROUP BY entity_id".format(where), where_data)]
|
||||
|
||||
@property
|
||||
def where_after_start_run(self):
|
||||
""" Returns SQL WHERE clause to select rows
|
||||
created after the start of the run. """
|
||||
return "created >= {} ".format(_adapt_datetime(self.start))
|
||||
|
||||
@property
|
||||
def where_limit_to_run(self):
|
||||
""" Return a SQL WHERE clause to limit results to this run. """
|
||||
where = self.where_after_start_run
|
||||
|
||||
if self.end is not None:
|
||||
where += "AND created < {} ".format(_adapt_datetime(self.end))
|
||||
|
||||
return where
|
||||
|
||||
|
||||
class Recorder(threading.Thread):
|
||||
"""
|
||||
Threaded recorder
|
||||
"""
|
||||
def __init__(self, hass):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.conn = None
|
||||
self.queue = queue.Queue()
|
||||
self.quit_object = object()
|
||||
self.lock = threading.Lock()
|
||||
self.recording_start = datetime.now()
|
||||
|
||||
def start_recording(event):
|
||||
""" Start recording. """
|
||||
self.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||
hass.bus.listen(MATCH_ALL, self.event_listener)
|
||||
|
||||
def run(self):
|
||||
""" Start processing events to save. """
|
||||
self._setup_connection()
|
||||
self._setup_run()
|
||||
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
|
||||
if event == self.quit_object:
|
||||
self._close_run()
|
||||
self._close_connection()
|
||||
return
|
||||
|
||||
elif event.event_type == EVENT_TIME_CHANGED:
|
||||
continue
|
||||
|
||||
elif event.event_type == EVENT_STATE_CHANGED:
|
||||
self.record_state(
|
||||
event.data['entity_id'], event.data.get('new_state'))
|
||||
|
||||
self.record_event(event)
|
||||
|
||||
def event_listener(self, event):
|
||||
""" Listens for new events on the EventBus and puts them
|
||||
in the process queue. """
|
||||
self.queue.put(event)
|
||||
|
||||
def shutdown(self, event):
|
||||
""" Tells the recorder to shut down. """
|
||||
self.queue.put(self.quit_object)
|
||||
|
||||
def record_state(self, entity_id, state):
|
||||
""" Save a state to the database. """
|
||||
now = datetime.now()
|
||||
|
||||
if state is None:
|
||||
info = (entity_id, '', "{}", now, now, now)
|
||||
else:
|
||||
info = (
|
||||
entity_id.lower(), state.state, json.dumps(state.attributes),
|
||||
state.last_changed, state.last_updated, now)
|
||||
|
||||
self.query(
|
||||
"INSERT INTO states ("
|
||||
"entity_id, state, attributes, last_changed, last_updated,"
|
||||
"created) VALUES (?, ?, ?, ?, ?, ?)", info)
|
||||
|
||||
def record_event(self, event):
|
||||
""" Save an event to the database. """
|
||||
info = (
|
||||
event.event_type, json.dumps(event.data, cls=JSONEncoder),
|
||||
str(event.origin), datetime.now()
|
||||
)
|
||||
|
||||
self.query(
|
||||
"INSERT INTO events ("
|
||||
"event_type, event_data, origin, created"
|
||||
") VALUES (?, ?, ?, ?)", info)
|
||||
|
||||
def query(self, sql_query, data=None, return_value=None):
|
||||
""" Query the database. """
|
||||
try:
|
||||
with self.conn, self.lock:
|
||||
_LOGGER.info("Running query %s", sql_query)
|
||||
|
||||
cur = self.conn.cursor()
|
||||
|
||||
if data is not None:
|
||||
cur.execute(sql_query, data)
|
||||
else:
|
||||
cur.execute(sql_query)
|
||||
|
||||
if return_value == RETURN_ROWCOUNT:
|
||||
return cur.rowcount
|
||||
elif return_value == RETURN_LASTROWID:
|
||||
return cur.lastrowid
|
||||
elif return_value == RETURN_ONE_ROW:
|
||||
return cur.fetchone()
|
||||
else:
|
||||
return cur.fetchall()
|
||||
|
||||
except sqlite3.IntegrityError:
|
||||
_LOGGER.exception(
|
||||
"Error querying the database using: %s", sql_query)
|
||||
return []
|
||||
|
||||
def _setup_connection(self):
|
||||
""" Ensure database is ready to fly. """
|
||||
db_path = self.hass.get_config_path(DB_FILE)
|
||||
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
# Make sure the database is closed whenever Python exits
|
||||
# without the STOP event being fired.
|
||||
atexit.register(self._close_connection)
|
||||
|
||||
# Have datetime objects be saved as integers
|
||||
sqlite3.register_adapter(datetime, _adapt_datetime)
|
||||
|
||||
# Validate we are on the correct schema or that we have to migrate
|
||||
cur = self.conn.cursor()
|
||||
|
||||
def save_migration(migration_id):
|
||||
""" Save and commit a migration to the database. """
|
||||
cur.execute('INSERT INTO schema_version VALUES (?, ?)',
|
||||
(migration_id, datetime.now()))
|
||||
self.conn.commit()
|
||||
_LOGGER.info("Database migrated to version %d", migration_id)
|
||||
|
||||
try:
|
||||
cur.execute('SELECT max(migration_id) FROM schema_version;')
|
||||
migration_id = cur.fetchone()[0] or 0
|
||||
|
||||
except sqlite3.OperationalError:
|
||||
# The table does not exist
|
||||
cur.execute('CREATE TABLE schema_version ('
|
||||
'migration_id integer primary key, performed integer)')
|
||||
migration_id = 0
|
||||
|
||||
if migration_id < 1:
|
||||
cur.execute("""
|
||||
CREATE TABLE recorder_runs (
|
||||
run_id integer primary key,
|
||||
start integer,
|
||||
end integer,
|
||||
closed_incorrect integer default 0,
|
||||
created integer)
|
||||
""")
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE events (
|
||||
event_id integer primary key,
|
||||
event_type text,
|
||||
event_data text,
|
||||
origin text,
|
||||
created integer)
|
||||
""")
|
||||
cur.execute(
|
||||
'CREATE INDEX events__event_type ON events(event_type)')
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE states (
|
||||
state_id integer primary key,
|
||||
entity_id text,
|
||||
state text,
|
||||
attributes text,
|
||||
last_changed integer,
|
||||
last_updated integer,
|
||||
created integer)
|
||||
""")
|
||||
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
|
||||
|
||||
save_migration(1)
|
||||
|
||||
def _close_connection(self):
|
||||
""" Close connection to the database. """
|
||||
_LOGGER.info("Closing database")
|
||||
atexit.unregister(self._close_connection)
|
||||
self.conn.close()
|
||||
|
||||
def _setup_run(self):
|
||||
""" Log the start of the current run. """
|
||||
if self.query("""UPDATE recorder_runs SET end=?, closed_incorrect=1
|
||||
WHERE end IS NULL""", (self.recording_start, ),
|
||||
return_value=RETURN_ROWCOUNT):
|
||||
|
||||
_LOGGER.warning("Found unfinished sessions")
|
||||
|
||||
self.query(
|
||||
"INSERT INTO recorder_runs (start, created) VALUES (?, ?)",
|
||||
(self.recording_start, datetime.now()))
|
||||
|
||||
def _close_run(self):
|
||||
""" Save end time for current run. """
|
||||
self.query(
|
||||
"UPDATE recorder_runs SET end=? WHERE start=?",
|
||||
(datetime.now(), self.recording_start))
|
||||
|
||||
|
||||
def _adapt_datetime(datetimestamp):
|
||||
""" Turn a datetime into an integer for in the DB. """
|
||||
return time.mktime(datetimestamp.timetuple())
|
||||
|
||||
|
||||
def _verify_instance():
|
||||
""" throws error if recorder not initialized. """
|
||||
if _INSTANCE is None:
|
||||
raise RuntimeError("Recorder not initialized.")
|
143
homeassistant/components/scheduler/__init__.py
Normal file
143
homeassistant/components/scheduler/__init__.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
homeassistant.components.scheduler
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
A component that will act as a scheduler and performe actions based
|
||||
on the events in the schedule.
|
||||
|
||||
It will read a json object from schedule.json in the config dir
|
||||
and create a schedule based on it.
|
||||
Each schedule is a JSON with the keys id, name, description,
|
||||
entity_ids, and events.
|
||||
- days is an array with the weekday number (monday=0) that the schdule
|
||||
is active
|
||||
- entity_ids an array with entity ids that the events in the schedule should
|
||||
effect (can also be groups)
|
||||
- events is an array of objects that describe the different events that is
|
||||
supported. Read in the events descriptions for more information
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
DOMAIN = 'scheduler'
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_SCHEDULE_FILE = 'schedule.json'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Create the schedules """
|
||||
|
||||
if DOMAIN in hass.components:
|
||||
return True
|
||||
|
||||
def setup_listener(schedule, event_data):
|
||||
""" Creates the event listener based on event_data """
|
||||
event_type = event_data['type']
|
||||
component = event_type
|
||||
|
||||
# if the event isn't part of a component
|
||||
if event_type in ['time']:
|
||||
component = 'scheduler.{}'.format(event_type)
|
||||
|
||||
elif component not in hass.components and \
|
||||
not bootstrap.setup_component(hass, component, config):
|
||||
|
||||
_LOGGER.warn("Could setup event listener for %s", component)
|
||||
return None
|
||||
|
||||
return get_component(component).create_event_listener(schedule,
|
||||
event_data)
|
||||
|
||||
def setup_schedule(schedule_data):
|
||||
""" setup a schedule based on the description """
|
||||
|
||||
schedule = Schedule(schedule_data['id'],
|
||||
name=schedule_data['name'],
|
||||
description=schedule_data['description'],
|
||||
entity_ids=schedule_data['entity_ids'],
|
||||
days=schedule_data['days'])
|
||||
|
||||
for event_data in schedule_data['events']:
|
||||
event_listener = setup_listener(schedule, event_data)
|
||||
|
||||
if event_listener:
|
||||
schedule.add_event_listener(event_listener)
|
||||
|
||||
schedule.schedule(hass)
|
||||
return True
|
||||
|
||||
with open(hass.get_config_path(_SCHEDULE_FILE)) as schedule_file:
|
||||
schedule_descriptions = json.load(schedule_file)
|
||||
|
||||
for schedule_description in schedule_descriptions:
|
||||
if not setup_schedule(schedule_description):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Schedule(object):
|
||||
""" A Schedule """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, schedule_id, name=None, description=None,
|
||||
entity_ids=None, days=None):
|
||||
|
||||
self.schedule_id = schedule_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
self.entity_ids = entity_ids or []
|
||||
|
||||
self.days = days or [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
self.__event_listeners = []
|
||||
|
||||
def add_event_listener(self, event_listener):
|
||||
""" Add a event to the schedule """
|
||||
self.__event_listeners.append(event_listener)
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule all the events in the schdule """
|
||||
for event in self.__event_listeners:
|
||||
event.schedule(hass)
|
||||
|
||||
|
||||
class EventListener(object):
|
||||
""" The base EventListner class that the schedule uses """
|
||||
def __init__(self, schedule):
|
||||
self.my_schedule = schedule
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
pass
|
||||
|
||||
def execute(self, hass):
|
||||
""" execute the event """
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ServiceEventListener(EventListener):
|
||||
""" A EventListner that calls a service when executed """
|
||||
|
||||
def __init__(self, schdule, service):
|
||||
EventListener.__init__(self, schdule)
|
||||
|
||||
(self.domain, self.service) = service.split('.')
|
||||
|
||||
def execute(self, hass):
|
||||
""" Call the service """
|
||||
data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids}
|
||||
hass.call_service(self.domain, self.service, data)
|
||||
|
||||
# Reschedule for next day
|
||||
self.schedule(hass)
|
69
homeassistant/components/scheduler/time.py
Normal file
69
homeassistant/components/scheduler/time.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
An event in the scheduler component that will call the service
|
||||
every specified day at the time specified.
|
||||
A time event need to have the type 'time', which service to call and at
|
||||
which time.
|
||||
|
||||
{
|
||||
"type": "time",
|
||||
"service": "switch.turn_off",
|
||||
"time": "22:00:00"
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.scheduler import ServiceEventListener
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_event_listener(schedule, event_listener_data):
|
||||
""" Create a TimeEvent based on the description """
|
||||
|
||||
service = event_listener_data['service']
|
||||
(hour, minute, second) = [int(x) for x in
|
||||
event_listener_data['time'].split(':')]
|
||||
|
||||
return TimeEventListener(schedule, service, hour, minute, second)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TimeEventListener(ServiceEventListener):
|
||||
""" The time event that the scheduler uses """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, schedule, service, hour, minute, second):
|
||||
ServiceEventListener.__init__(self, schedule, service)
|
||||
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule this event so that it will be called """
|
||||
|
||||
next_time = datetime.now().replace(hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=0)
|
||||
|
||||
# Calculate the next time the event should be executed.
|
||||
# That is the next day that the schedule is configured to run
|
||||
while next_time < datetime.now() or \
|
||||
next_time.weekday() not in self.my_schedule.days:
|
||||
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(now):
|
||||
""" Call the execute method """
|
||||
self.execute(hass)
|
||||
|
||||
hass.track_point_in_time(execute, next_time)
|
||||
|
||||
_LOGGER.info(
|
||||
'TimeEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time, self.domain, self.service)
|
@ -3,6 +3,23 @@ homeassistant.components.sun
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to keep track of the sun.
|
||||
|
||||
|
||||
Event listener
|
||||
--------------
|
||||
The suns event listener will call the service
|
||||
when the sun rises or sets with an offset.
|
||||
The sun evnt need to have the type 'sun', which service to call,
|
||||
which event (sunset or sunrise) and the offset.
|
||||
|
||||
{
|
||||
"type": "sun",
|
||||
"service": "switch.turn_on",
|
||||
"event": "sunset",
|
||||
"offset": "-01:00:00"
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
@ -12,6 +29,8 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import str_to_datetime, datetime_to_str
|
||||
|
||||
from homeassistant.components.scheduler import ServiceEventListener
|
||||
|
||||
DEPENDENCIES = []
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
@ -22,6 +41,8 @@ STATE_BELOW_HORIZON = "below_horizon"
|
||||
STATE_ATTR_NEXT_RISING = "next_rising"
|
||||
STATE_ATTR_NEXT_SETTING = "next_setting"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if the sun is currently up based on the statemachine. """
|
||||
@ -137,3 +158,95 @@ def setup(hass, config):
|
||||
update_sun_state(datetime.now())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_event_listener(schedule, event_listener_data):
|
||||
""" Create a sun event listener based on the description. """
|
||||
|
||||
negative_offset = False
|
||||
service = event_listener_data['service']
|
||||
offset_str = event_listener_data['offset']
|
||||
event = event_listener_data['event']
|
||||
|
||||
if offset_str.startswith('-'):
|
||||
negative_offset = True
|
||||
offset_str = offset_str[1:]
|
||||
|
||||
(hour, minute, second) = [int(x) for x in offset_str.split(':')]
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if event == 'sunset':
|
||||
return SunsetEventListener(schedule, service, offset, negative_offset)
|
||||
|
||||
return SunriseEventListener(schedule, service, offset, negative_offset)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunEventListener(ServiceEventListener):
|
||||
""" This is the base class for sun event listeners. """
|
||||
|
||||
def __init__(self, schedule, service, offset, negative_offset):
|
||||
ServiceEventListener.__init__(self, schedule, service)
|
||||
|
||||
self.offset = offset
|
||||
self.negative_offset = negative_offset
|
||||
|
||||
def __get_next_time(self, next_event):
|
||||
"""
|
||||
Returns when the next time the service should be called.
|
||||
Taking into account the offset and which days the event should execute.
|
||||
"""
|
||||
|
||||
if self.negative_offset:
|
||||
next_time = next_event - self.offset
|
||||
else:
|
||||
next_time = next_event + self.offset
|
||||
|
||||
while next_time < datetime.now() or \
|
||||
next_time.weekday() not in self.my_schedule.days:
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def schedule_next_event(self, hass, next_event):
|
||||
""" Schedule the event """
|
||||
next_time = self.__get_next_time(next_event)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(now):
|
||||
""" Call the execute method """
|
||||
self.execute(hass)
|
||||
|
||||
hass.track_point_in_time(execute, next_time)
|
||||
|
||||
return next_time
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunsetEventListener(SunEventListener):
|
||||
""" This class is used the call a service when the sun sets. """
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
next_setting_dt = next_setting(hass)
|
||||
|
||||
next_time_dt = self.schedule_next_event(hass, next_setting_dt)
|
||||
|
||||
_LOGGER.info(
|
||||
'SunsetEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time_dt, self.domain, self.service)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SunriseEventListener(SunEventListener):
|
||||
""" This class is used the call a service when the sun rises. """
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
next_rising_dt = next_rising(hass)
|
||||
|
||||
next_time_dt = self.schedule_next_event(hass, next_rising_dt)
|
||||
|
||||
_LOGGER.info(
|
||||
'SunriseEventListener scheduled for %s, will call service %s.%s',
|
||||
next_time_dt, self.domain, self.service)
|
||||
|
@ -1,14 +1,10 @@
|
||||
""" Support for Tellstick switches. """
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
|
||||
try:
|
||||
import tellcore.constants as tc_constants
|
||||
except ImportError:
|
||||
# Don't care for now. Warning will come when get_switches is called.
|
||||
pass
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
|
||||
def get_devices(hass, config):
|
||||
@ -21,15 +17,21 @@ def get_devices(hass, config):
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
switches = core.devices()
|
||||
switches_and_lights = core.devices()
|
||||
|
||||
return [TellstickSwitch(switch) for switch in switches]
|
||||
switches = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
switches.append(TellstickSwitchDevice(switch))
|
||||
|
||||
return switches
|
||||
|
||||
|
||||
class TellstickSwitch(ToggleDevice):
|
||||
class TellstickSwitchDevice(ToggleDevice):
|
||||
""" represents a Tellstick switch within home assistant. """
|
||||
last_sent_command_mask = (tc_constants.TELLSTICK_TURNON |
|
||||
tc_constants.TELLSTICK_TURNOFF)
|
||||
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||
tellcore_constants.TELLSTICK_TURNOFF)
|
||||
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
@ -51,7 +53,7 @@ class TellstickSwitch(ToggleDevice):
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
return last_command == tc_constants.TELLSTICK_TURNON
|
||||
return last_command == tellcore_constants.TELLSTICK_TURNON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
|
@ -99,3 +99,4 @@ URL_API_EVENTS_EVENT = "/api/events/{}"
|
||||
URL_API_SERVICES = "/api/services"
|
||||
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
|
||||
URL_API_EVENT_FORWARD = "/api/event_forwarding"
|
||||
URL_API_COMPONENTS = "/api/components"
|
||||
|
@ -21,7 +21,7 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||
current_ids = hass.states.entity_ids()
|
||||
|
||||
return ensure_unique_string(
|
||||
entity_id_format.format(slugify(name)), current_ids)
|
||||
entity_id_format.format(slugify(name.lower())), current_ids)
|
||||
|
||||
|
||||
def extract_entity_ids(hass, service):
|
||||
|
@ -147,6 +147,7 @@ def load_order_components(components):
|
||||
Takes in a list of components we want to load:
|
||||
- filters out components we cannot load
|
||||
- filters out components that have invalid/circular dependencies
|
||||
- Will make sure the recorder component is loaded first
|
||||
- Will ensure that all components that do not directly depend on
|
||||
the group component will be loaded before the group component.
|
||||
- returns an OrderedSet load order.
|
||||
@ -154,6 +155,7 @@ def load_order_components(components):
|
||||
_check_prepared()
|
||||
|
||||
group = get_component('group')
|
||||
recorder = get_component('recorder')
|
||||
|
||||
load_order = OrderedSet()
|
||||
|
||||
@ -171,6 +173,10 @@ def load_order_components(components):
|
||||
group and group.DOMAIN in order):
|
||||
load_order.update(comp_load_order)
|
||||
|
||||
# Push recorder to first place in load order
|
||||
if recorder.DOMAIN in load_order:
|
||||
load_order.promote(recorder.DOMAIN)
|
||||
|
||||
return load_order
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ import urllib.parse
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, AUTH_HEADER, URL_API, URL_API_STATES, URL_API_STATES_ENTITY,
|
||||
@ -110,13 +111,13 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
self.bus = EventBus(remote_api, pool)
|
||||
self.services = ha.ServiceRegistry(self.bus, pool)
|
||||
self.states = StateMachine(self.bus, self.remote_api)
|
||||
self.components = []
|
||||
|
||||
def start(self):
|
||||
# Ensure a local API exists to connect with remote
|
||||
if self.local_api is None:
|
||||
import homeassistant.components.http as http
|
||||
|
||||
http.setup(self)
|
||||
bootstrap.setup_component(self, 'http')
|
||||
bootstrap.setup_component(self, 'api')
|
||||
|
||||
ha.Timer(self)
|
||||
|
||||
@ -257,10 +258,20 @@ class JSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
""" Converts Home Assistant objects and hands
|
||||
other objects to the original method. """
|
||||
if isinstance(obj, ha.State):
|
||||
if isinstance(obj, (ha.State, ha.Event)):
|
||||
return obj.as_dict()
|
||||
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
try:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
except TypeError:
|
||||
# If the JSON serializer couldn't serialize it
|
||||
# it might be a generator, convert it to a list
|
||||
try:
|
||||
return [self.default(child_obj)
|
||||
for child_obj in obj]
|
||||
except TypeError:
|
||||
# Ok, we're lost, cause the original error
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def validate_api(api):
|
||||
|
@ -216,12 +216,21 @@ class OrderedSet(collections.MutableSet):
|
||||
return key in self.map
|
||||
|
||||
def add(self, key):
|
||||
""" Add an element to the set. """
|
||||
""" Add an element to the end of the set. """
|
||||
if key not in self.map:
|
||||
end = self.end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.map[key] = [key, curr, end]
|
||||
|
||||
def promote(self, key):
|
||||
""" Promote element to beginning of the set, add if not there. """
|
||||
if key in self.map:
|
||||
self.discard(key)
|
||||
|
||||
begin = self.end[2]
|
||||
curr = begin[1]
|
||||
curr[2] = begin[1] = self.map[key] = [key, curr, begin]
|
||||
|
||||
def discard(self, key):
|
||||
""" Discard an element from the set. """
|
||||
if key in self.map:
|
||||
|
@ -3,11 +3,13 @@ if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
scripts/build_js
|
||||
|
||||
# To build the frontend, you need node, bower and vulcanize
|
||||
# npm install -g bower vulcanize
|
||||
|
||||
# Install dependencies
|
||||
cd homeassistant/components/http/www_static/polymer
|
||||
cd homeassistant/components/frontend/www_static/polymer
|
||||
bower install
|
||||
cd ..
|
||||
cp polymer/bower_components/webcomponentsjs/webcomponents.min.js .
|
||||
@ -15,7 +17,7 @@ cp polymer/bower_components/webcomponentsjs/webcomponents.min.js .
|
||||
# Let Polymer refer to the minified JS version before we compile
|
||||
sed -i.bak 's/polymer\.js/polymer\.min\.js/' polymer/bower_components/polymer/polymer.html
|
||||
|
||||
vulcanize -o frontend.html --inline --strip polymer/splash-login.html
|
||||
vulcanize -o frontend.html --inline --strip polymer/home-assistant.html
|
||||
|
||||
# Revert back the change to the Polymer component
|
||||
rm polymer/bower_components/polymer/polymer.html
|
||||
@ -23,11 +25,11 @@ mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/po
|
||||
|
||||
# Generate the MD5 hash of the new frontend
|
||||
cd ..
|
||||
echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > frontend.py
|
||||
echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > version.py
|
||||
if [ $(command -v md5) ]; then
|
||||
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py
|
||||
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> version.py
|
||||
elif [ $(command -v md5sum) ]; then
|
||||
echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> frontend.py
|
||||
echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py
|
||||
else
|
||||
echo 'Could not find a MD5 utility'
|
||||
fi
|
||||
|
9
scripts/build_js
Executable file
9
scripts/build_js
Executable file
@ -0,0 +1,9 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
cd homeassistant/components/frontend/www_static/polymer/home-assistant-js
|
||||
|
||||
npm install
|
||||
npm run prod
|
9
scripts/dev_js
Executable file
9
scripts/dev_js
Executable file
@ -0,0 +1,9 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
cd homeassistant/components/frontend/www_static/polymer/home-assistant-js
|
||||
|
||||
npm install
|
||||
npm run dev
|
@ -5,13 +5,13 @@ tests.test_component_http
|
||||
Tests Home Assistant HTTP component does what it should do.
|
||||
"""
|
||||
# pylint: disable=protected-access,too-many-public-methods
|
||||
import re
|
||||
import unittest
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.remote as remote
|
||||
import homeassistant.components.http as http
|
||||
|
||||
@ -43,9 +43,12 @@ def setUpModule(): # pylint: disable=invalid-name
|
||||
hass.bus.listen('test_event', lambda _: _)
|
||||
hass.states.set('test.test', 'a_state')
|
||||
|
||||
http.setup(hass,
|
||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||
http.CONF_SERVER_PORT: SERVER_PORT}})
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||
http.CONF_SERVER_PORT: SERVER_PORT}})
|
||||
|
||||
bootstrap.setup_component(hass, 'api')
|
||||
|
||||
hass.start()
|
||||
|
||||
@ -55,50 +58,17 @@ def tearDownModule(): # pylint: disable=invalid-name
|
||||
hass.stop()
|
||||
|
||||
|
||||
class TestHTTP(unittest.TestCase):
|
||||
""" Test the HTTP debug interface and API. """
|
||||
class TestAPI(unittest.TestCase):
|
||||
""" Test the API. """
|
||||
|
||||
def test_frontend_and_static(self):
|
||||
""" Tests if we can get the frontend. """
|
||||
req = requests.get(_url(""))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
# Test we can retrieve frontend.js
|
||||
frontendjs = re.search(
|
||||
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
|
||||
req.text)
|
||||
|
||||
self.assertIsNotNone(frontendjs)
|
||||
|
||||
req = requests.head(_url(frontendjs.groups(0)[0]))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
# Test auto filling in api password
|
||||
req = requests.get(
|
||||
_url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD)))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text)
|
||||
|
||||
self.assertIsNotNone(auth_text)
|
||||
|
||||
# Test 404
|
||||
self.assertEqual(404, requests.get(_url("/not-existing")).status_code)
|
||||
|
||||
# Test we cannot POST to /
|
||||
self.assertEqual(405, requests.post(_url("")).status_code)
|
||||
|
||||
def test_api_password(self):
|
||||
""" Test if we get access denied if we omit or provide
|
||||
a wrong api password. """
|
||||
# TODO move back to http component and test with use_auth.
|
||||
def test_access_denied_without_password(self):
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")))
|
||||
|
||||
self.assertEqual(401, req.status_code)
|
||||
|
||||
def test_access_denied_with_wrong_password(self):
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")),
|
||||
headers={remote.AUTH_HEADER: 'wrongpassword'})
|
96
tests/test_component_frontend.py
Normal file
96
tests/test_component_frontend.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""
|
||||
tests.test_component_http
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests Home Assistant HTTP component does what it should do.
|
||||
"""
|
||||
# pylint: disable=protected-access,too-many-public-methods
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.remote as remote
|
||||
import homeassistant.components.http as http
|
||||
import homeassistant.components.frontend as frontend
|
||||
|
||||
API_PASSWORD = "test1234"
|
||||
|
||||
# Somehow the socket that holds the default port does not get released
|
||||
# when we close down HA in a different test case. Until I have figured
|
||||
# out what is going on, let's run this test on a different port.
|
||||
SERVER_PORT = 8121
|
||||
|
||||
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
|
||||
|
||||
HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD}
|
||||
|
||||
hass = None
|
||||
|
||||
|
||||
def _url(path=""):
|
||||
""" Helper method to generate urls. """
|
||||
return HTTP_BASE_URL + path
|
||||
|
||||
|
||||
def setUpModule(): # pylint: disable=invalid-name
|
||||
""" Initalizes a Home Assistant server. """
|
||||
global hass
|
||||
|
||||
hass = ha.HomeAssistant()
|
||||
|
||||
hass.bus.listen('test_event', lambda _: _)
|
||||
hass.states.set('test.test', 'a_state')
|
||||
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||
http.CONF_SERVER_PORT: SERVER_PORT}})
|
||||
|
||||
bootstrap.setup_component(hass, 'frontend')
|
||||
|
||||
hass.start()
|
||||
|
||||
|
||||
def tearDownModule(): # pylint: disable=invalid-name
|
||||
""" Stops the Home Assistant server. """
|
||||
hass.stop()
|
||||
|
||||
|
||||
class TestFrontend(unittest.TestCase):
|
||||
""" Test the frontend. """
|
||||
|
||||
def test_frontend_and_static(self):
|
||||
""" Tests if we can get the frontend. """
|
||||
req = requests.get(_url(""))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
# Test we can retrieve frontend.js
|
||||
frontendjs = re.search(
|
||||
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
|
||||
req.text)
|
||||
|
||||
self.assertIsNotNone(frontendjs)
|
||||
|
||||
req = requests.head(_url(frontendjs.groups(0)[0]))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
def test_auto_filling_in_api_password(self):
|
||||
req = requests.get(
|
||||
_url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD)))
|
||||
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text)
|
||||
|
||||
self.assertIsNotNone(auth_text)
|
||||
|
||||
def test_404(self):
|
||||
self.assertEqual(404, requests.get(_url("/not-existing")).status_code)
|
||||
|
||||
def test_we_cannot_POST_to_root(self):
|
||||
self.assertEqual(405, requests.post(_url("")).status_code)
|
@ -233,18 +233,18 @@ class TestStateMachine(unittest.TestCase):
|
||||
""" Test get_entity_ids method. """
|
||||
ent_ids = self.states.entity_ids()
|
||||
self.assertEqual(2, len(ent_ids))
|
||||
self.assertTrue('light.Bowl' in ent_ids)
|
||||
self.assertTrue('switch.AC' in ent_ids)
|
||||
self.assertTrue('light.bowl' in ent_ids)
|
||||
self.assertTrue('switch.ac' in ent_ids)
|
||||
|
||||
ent_ids = self.states.entity_ids('light')
|
||||
self.assertEqual(1, len(ent_ids))
|
||||
self.assertTrue('light.Bowl' in ent_ids)
|
||||
self.assertTrue('light.bowl' in ent_ids)
|
||||
|
||||
def test_remove(self):
|
||||
""" Test remove method. """
|
||||
self.assertTrue('light.Bowl' in self.states.entity_ids())
|
||||
self.assertTrue(self.states.remove('light.Bowl'))
|
||||
self.assertFalse('light.Bowl' in self.states.entity_ids())
|
||||
self.assertTrue('light.bowl' in self.states.entity_ids())
|
||||
self.assertTrue(self.states.remove('light.bowl'))
|
||||
self.assertFalse('light.bowl' in self.states.entity_ids())
|
||||
|
||||
# If it does not exist, we should get False
|
||||
self.assertFalse(self.states.remove('light.Bowl'))
|
||||
|
@ -10,6 +10,7 @@ Uses port 8125 as a port that nothing runs on
|
||||
import unittest
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.remote as remote
|
||||
import homeassistant.components.http as http
|
||||
|
||||
@ -36,9 +37,12 @@ def setUpModule(): # pylint: disable=invalid-name
|
||||
hass.bus.listen('test_event', lambda _: _)
|
||||
hass.states.set('test.test', 'a_state')
|
||||
|
||||
http.setup(hass,
|
||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||
http.CONF_SERVER_PORT: 8122}})
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||
http.CONF_SERVER_PORT: 8122}})
|
||||
|
||||
bootstrap.setup_component(hass, 'api')
|
||||
|
||||
hass.start()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user