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:
Paulus Schoutsen 2015-02-07 21:45:59 -08:00
commit eea4bb7118
87 changed files with 3323 additions and 2002 deletions

2
.gitignore vendored
View File

@ -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
View File

@ -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

View File

@ -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!"}

View File

@ -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):

View File

@ -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))

View File

@ -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)

View 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)

View File

@ -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

View 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)

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "43699d5ec727d3444985a1028d21e0d9"
VERSION = "db6b9c263c4be99af5b25b8c1cb20e57"

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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);
},
});

View File

@ -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);
},
});

View File

@ -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";

View File

@ -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) {

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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]
});
}

View File

@ -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);

View File

@ -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>

View File

@ -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>

File diff suppressed because one or more lines are too long

View File

@ -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

View 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())

View File

@ -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

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View 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

View 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.")

View 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)

View 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)

View File

@ -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)

View File

@ -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. """

View File

@ -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"

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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:

View File

@ -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
View 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
View 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

View File

@ -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'})

View 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)

View File

@ -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'))

View File

@ -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()