From a2f8fa7b05cde251cea2d6dc1d8de4e9a6e8592d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 14:39:47 -0700 Subject: [PATCH 1/7] Add time_fired to Event class --- homeassistant/__init__.py | 24 ++++++++++++++++++++---- homeassistant/components/recorder.py | 21 ++++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index b93a8ee99be..d1cb17f22c6 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) import homeassistant.util as util DOMAIN = "homeassistant" @@ -325,19 +325,23 @@ class EventOrigin(enum.Enum): class Event(object): """ Represents an event within the Bus. """ - __slots__ = ['event_type', 'data', 'origin'] + __slots__ = ['event_type', 'data', 'origin', 'time_fired'] - def __init__(self, event_type, data=None, origin=EventOrigin.local): + def __init__(self, event_type, data=None, origin=EventOrigin.local, + time_fired=None): self.event_type = event_type self.data = data or {} self.origin = origin + self.time_fired = util.strip_microseconds( + time_fired or dt.datetime.now()) 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) + 'origin': str(self.origin), + 'time_fired': util.datetime_to_str(self.time_fired), } def __repr__(self): @@ -482,6 +486,18 @@ class State(object): """ Returns domain of this state. """ return util.split_entity_id(self.entity_id)[0] + @property + def object_id(self): + """ Returns object_id of this state. """ + return util.split_entity_id(self.entity_id)[1] + + @property + def name(self): + """ Name to represent this state. """ + return ( + self.attributes.get(ATTR_FRIENDLY_NAME) or + self.object_id.replace('_', ' ')) + def copy(self): """ Creates a copy of itself. """ return State(self.entity_id, self.state, diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 367af7d2d73..b71ed06203f 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -70,9 +70,10 @@ def row_to_state(row): 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()]) + return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()], + datetime.fromtimestamp(row[5])) except ValueError: - # When json.oads fails + # When json.loads fails _LOGGER.exception("Error converting row to event: %s", row) return None @@ -225,13 +226,13 @@ class Recorder(threading.Thread): """ Save an event to the database. """ info = ( event.event_type, json.dumps(event.data, cls=JSONEncoder), - str(event.origin), datetime.now() + str(event.origin), datetime.now(), event.time_fired, ) self.query( "INSERT INTO events (" - "event_type, event_data, origin, created" - ") VALUES (?, ?, ?, ?)", info) + "event_type, event_data, origin, created, time_fired" + ") VALUES (?, ?, ?, ?, ?)", info) def query(self, sql_query, data=None, return_value=None): """ Query the database. """ @@ -328,6 +329,16 @@ class Recorder(threading.Thread): save_migration(1) + if migration_id < 2: + cur.execute(""" + ALTER TABLE events + ADD COLUMN time_fired integer + """) + + cur.execute('UPDATE events SET time_fired=created') + + save_migration(2) + def _close_connection(self): """ Close connection to the database. """ _LOGGER.info("Closing database") From 234bfe1199655ac7a7aeff613450679af62756e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 14:43:16 -0700 Subject: [PATCH 2/7] Add logbook component --- homeassistant/components/logbook.py | 127 +++++++++++++++++++++++++++ homeassistant/components/recorder.py | 3 +- homeassistant/remote.py | 2 +- 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/logbook.py diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py new file mode 100644 index 00000000000..b5739638a43 --- /dev/null +++ b/homeassistant/components/logbook.py @@ -0,0 +1,127 @@ +""" +homeassistant.components.logbook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Parses events and generates a human log +""" +from datetime import datetime + +from homeassistant import State, DOMAIN as HA_DOMAIN +from homeassistant.const import ( + EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.util as util +import homeassistant.components.recorder as recorder +import homeassistant.components.sun as sun + +DOMAIN = "logbook" +DEPENDENCIES = ['recorder', 'http'] + +URL_LOGBOOK = '/api/logbook' + +QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?" +QUERY_EVENTS_BETWEEN = """ + SELECT * FROM events WHERE time_fired > ? AND time_fired < ? + ORDER BY time_fired +""" + + +def setup(hass, config): + """ Listens for download events to download files. """ + hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook) + + return True + + +def _handle_get_logbook(handler, path_match, data): + """ Return logbook entries. """ + start_today = datetime.now().date() + import time + print(time.mktime(start_today.timetuple())) + handler.write_json(humanify( + recorder.query_events(QUERY_EVENTS_AFTER, (start_today,)))) + + +class Entry(object): + """ A human readable version of the log. """ + + # pylint: disable=too-many-arguments + + def __init__(self, when=None, name=None, message=None, domain=None, + entity_id=None): + self.when = when + self.name = name + self.message = message + self.domain = domain + self.entity_id = entity_id + + @property + def is_valid(self): + """ Returns if this entry contains all the needed fields. """ + return self.when and self.name and self.message + + def as_dict(self): + """ Convert Entry to a dict to be used within JSON. """ + return { + 'when': util.datetime_to_str(self.when), + 'name': self.name, + 'message': self.message, + 'domain': self.domain, + 'entity_id': self.entity_id, + } + + +def humanify(events): + """ Generator that converts a list of events into Entry objects. """ + # pylint: disable=too-many-branches + for event in events: + if event.event_type == EVENT_STATE_CHANGED: + + # Do not report on new entities + if 'old_state' not in event.data: + continue + + to_state = State.from_dict(event.data.get('new_state')) + + if not to_state: + continue + + domain = to_state.domain + + entry = Entry( + event.time_fired, domain=domain, + name=to_state.name, entity_id=to_state.entity_id) + + if domain == 'device_tracker': + entry.message = '{} home'.format( + 'arrived' if to_state.state == STATE_HOME else 'left') + + elif domain == 'sun': + if to_state.state == sun.STATE_ABOVE_HORIZON: + entry.message = 'has risen' + else: + entry.message = 'has set' + + elif to_state.state == STATE_ON: + # Future: combine groups and its entity entries ? + entry.message = "turned on" + + elif to_state.state == STATE_OFF: + entry.message = "turned off" + + else: + entry.message = "changed to {}".format(to_state.state) + + if entry.is_valid: + yield entry + + elif event.event_type == EVENT_HOMEASSISTANT_START: + # Future: look for sequence stop/start and rewrite as restarted + yield Entry( + event.time_fired, "Home Assistant", "started", + domain=HA_DOMAIN) + + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + yield Entry( + event.time_fired, "Home Assistant", "stopped", + domain=HA_DOMAIN) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index b71ed06203f..f2e5fa35ad4 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -9,7 +9,7 @@ import logging import threading import queue import sqlite3 -from datetime import datetime +from datetime import datetime, date import time import json import atexit @@ -272,6 +272,7 @@ class Recorder(threading.Thread): atexit.register(self._close_connection) # Have datetime objects be saved as integers + sqlite3.register_adapter(date, _adapt_datetime) sqlite3.register_adapter(datetime, _adapt_datetime) # Validate we are on the correct schema or that we have to migrate diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 19aa86f67b9..bd576f50e48 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -262,7 +262,7 @@ 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, ha.Event)): + if hasattr(obj, 'as_dict'): return obj.as_dict() try: From 100081757c311f6ce3ac4e9c1f6bbcc1b0e2302e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 14:50:41 -0700 Subject: [PATCH 3/7] Add logbook to frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 49 +++++++++++---- .../polymer/components/display-time.html | 25 ++++++++ .../polymer/components/ha-logbook.html | 17 ++++++ .../polymer/components/logbook-entry.html | 60 +++++++++++++++++++ .../www_static/polymer/home-assistant-js | 2 +- .../polymer/layouts/home-assistant-main.html | 14 ++++- .../polymer/layouts/partial-logbook.html | 56 +++++++++++++++++ .../resources/home-assistant-icons.html | 2 +- .../resources/home-assistant-style.html | 25 ++++++++ 10 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/polymer/components/display-time.html create mode 100644 homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html create mode 100644 homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html create mode 100644 homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index f580952cfeb..ea306279316 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "a063d1482fd49e9297d64e1329324f1c" +VERSION = "b06d3667e9e461173029ded9c0c9b815" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index aaf95520da6..32b16c2fb6a 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -4,7 +4,30 @@ var ab=H,bb=L;a.esprima={parse:R}}(this),function(a){"use strict";function b(a,b b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.localName,a);for(var c in a){var d=a[c];PolymerGestures.addEventListener(this,c,this.element.getEventHandler(this,this,d))}},dispatchMethod:function(a,c,d){if(a){b.events&&console.group("[%s] dispatch [%s]",a.localName,c);var e="function"==typeof c?c:a[c];e&&e[d?"apply":"call"](a,d),b.events&&console.groupEnd(),Polymer.flush()}}};a.api.instance.events=d,a.addEventListener=function(a,b,c,d){PolymerGestures.addEventListener(wrap(a),b,c,d)},a.removeEventListener=function(a,b,c,d){PolymerGestures.removeEventListener(wrap(a),b,c,d)}}(Polymer),function(a){var b={copyInstanceAttributes:function(){var a=this._instanceAttributes;for(var b in a)this.hasAttribute(b)||this.setAttribute(b,a[b])},takeAttributes:function(){if(this._publishLC)for(var a,b=0,c=this.attributes,d=c.length;(a=c[b])&&d>b;b++)this.attributeToProperty(a.name,a.value)},attributeToProperty:function(b,c){var b=this.propertyForAttribute(b);if(b){if(c&&c.search(a.bindPattern)>=0)return;var d=this[b],c=this.deserializeValue(c,d);c!==d&&(this[b]=c)}},propertyForAttribute:function(a){var b=this._publishLC&&this._publishLC[a];return b},deserializeValue:function(b,c){return a.deserializeValue(b,c)},serializeValue:function(a,b){return"boolean"===b?a?"":void 0:"object"!==b&&"function"!==b&&void 0!==a?a:void 0},reflectPropertyToAttribute:function(a){var b=typeof this[a],c=this.serializeValue(this[a],b);void 0!==c?this.setAttribute(a,c):"boolean"===b&&this.removeAttribute(a)}};a.api.instance.attributes=b}(Polymer),function(a){function b(a,b){return a===b?0!==a||1/a===1/b:f(a)&&f(b)?!0:a!==a&&b!==b}function c(a,b){return void 0===b&&null===a?b:null===b||void 0===b?a:b}var d=window.WebComponents?WebComponents.flags.log:{},e={object:void 0,type:"update",name:void 0,oldValue:void 0},f=Number.isNaN||function(a){return"number"==typeof a&&isNaN(a)},g={createPropertyObserver:function(){var a=this._observeNames;if(a&&a.length){var b=this._propertyObserver=new CompoundObserver(!0);this.registerObserver(b);for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.addPath(this,c),this.observeArrayValue(c,this[c],null)}},openPropertyObserver:function(){this._propertyObserver&&this._propertyObserver.open(this.notifyPropertyChanges,this)},notifyPropertyChanges:function(a,b,c){var d,e,f={};for(var g in b)if(d=c[2*g+1],e=this.observe[d]){var h=b[g],i=a[g];this.observeArrayValue(d,i,h),f[e]||(void 0!==h&&null!==h||void 0!==i&&null!==i)&&(f[e]=!0,this.invokeMethod(e,[h,i,arguments]))}},invokeMethod:function(a,b){var c=this[a]||a;"function"==typeof c&&c.apply(this,b)},deliverChanges:function(){this._propertyObserver&&this._propertyObserver.deliver()},observeArrayValue:function(a,b,c){var e=this.observe[a];if(e&&(Array.isArray(c)&&(d.observe&&console.log("[%s] observeArrayValue: unregister observer [%s]",this.localName,a),this.closeNamedObserver(a+"__array")),Array.isArray(b))){d.observe&&console.log("[%s] observeArrayValue: register observer [%s]",this.localName,a,b);var f=new ArrayObserver(b);f.open(function(a){this.invokeMethod(e,[a])},this),this.registerNamedObserver(a+"__array",f)}},emitPropertyChangeRecord:function(a,c,d){if(!b(c,d)&&(this._propertyChanged(a,c,d),Observer.hasObjectObserve)){var f=this._objectNotifier;f||(f=this._objectNotifier=Object.getNotifier(this)),e.object=this,e.name=a,e.oldValue=d,f.notify(e)}},_propertyChanged:function(a){this.reflect[a]&&this.reflectPropertyToAttribute(a)},bindProperty:function(a,b,d){if(d)return void(this[a]=b);var e=this.element.prototype.computed;if(e&&e[a]){var f=a+"ComputedBoundObservable_";return void(this[f]=b)}return this.bindToAccessor(a,b,c)},bindToAccessor:function(a,c,d){function e(b,c){j[f]=b;var d=j[h];d&&"function"==typeof d.setValue&&d.setValue(b),j.emitPropertyChangeRecord(a,b,c)}var f=a+"_",g=a+"Observable_",h=a+"ComputedBoundObservable_";this[g]=c;var i=this[f],j=this,k=c.open(e);if(d&&!b(i,k)){var l=d(i,k);b(k,l)||(k=l,c.setValue&&c.setValue(k))}e(k,i);var m={close:function(){c.close(),j[g]=void 0,j[h]=void 0}};return this.registerObserver(m),m},createComputedProperties:function(){if(this._computedNames)for(var a=0;ae&&(c=d[e]);e++)b[c.id]=c},onMutation:function(a,b){var c=new MutationObserver(function(a){b.call(this,c,a),c.disconnect()}.bind(this));c.observe(a,{childList:!0,subtree:!0})}};c.prototype=d,d.constructor=c,a.Base=c,a.isBase=b,a.api.instance.base=d}(Polymer),function(a){function b(a){return a.__proto__}function c(a,b){var c="",d=!1;b&&(c=b.localName,d=b.hasAttribute("is"));var e=WebComponents.ShadowCSS.makeScopeSelector(c,d);return WebComponents.ShadowCSS.shimCssText(a,e)}var d=(window.WebComponents?WebComponents.flags.log:{},window.ShadowDOMPolyfill),e="element",f="controller",g={STYLE_SCOPE_ATTRIBUTE:e,installControllerStyles:function(){var a=this.findStyleScope();if(a&&!this.scopeHasNamedStyle(a,this.localName)){for(var c=b(this),d="";c&&c.element;)d+=c.element.cssTextForScope(f),c=b(c);d&&this.installScopeCssText(d,a)}},installScopeStyle:function(a,b,c){var c=c||this.findStyleScope(),b=b||"";if(c&&!this.scopeHasNamedStyle(c,this.localName+b)){var d="";if(a instanceof Array)for(var e,f=0,g=a.length;g>f&&(e=a[f]);f++)d+=e.textContent+"\n\n";else d=a.textContent;this.installScopeCssText(d,c,b)}},installScopeCssText:function(a,b,e){if(b=b||this.findStyleScope(),e=e||"",b){d&&(a=c(a,b.host));var g=this.element.cssTextToScopeStyle(a,f);Polymer.applyStyleToScope(g,b),this.styleCacheForScope(b)[this.localName+e]=!0}},findStyleScope:function(a){for(var b=a||this;b.parentNode;)b=b.parentNode;return b},scopeHasNamedStyle:function(a,b){var c=this.styleCacheForScope(a);return c[b]},styleCacheForScope:function(a){if(d){var b=a.host?a.host.localName:a.localName;return h[b]||(h[b]={})}return a._scopeStyles=a._scopeStyles||{}}},h={};a.api.instance.styles=g}(Polymer),function(a){function b(a,b){if("string"!=typeof a){var c=b||document._currentScript;if(b=a,a=c&&c.parentNode&&c.parentNode.getAttribute?c.parentNode.getAttribute("name"):"",!a)throw"Element name could not be inferred."}if(f(a))throw"Already registered (Polymer) prototype for element "+a;e(a,b),d(a)}function c(a,b){i[a]=b}function d(a){i[a]&&(i[a].registerWhenReady(),delete i[a])}function e(a,b){return j[a]=b||{}}function f(a){return j[a]}function g(a,b){if("string"!=typeof b)return!1;var c=HTMLElement.getPrototypeForTag(b),d=c&&c.constructor;return d?CustomElements["instanceof"]?CustomElements["instanceof"](a,d):a instanceof d:!1}var h=a.extend,i=(a.api,{}),j={};a.getRegisteredPrototype=f,a.waitingForPrototype=c,a.instanceOfType=g,window.Polymer=b,h(Polymer,a),WebComponents.consumeDeclarations&&WebComponents.consumeDeclarations(function(a){if(a)for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.apply(null,c)})}(Polymer),function(a){var b={resolveElementPaths:function(a){Polymer.urlResolver.resolveDom(a)},addResolvePathApi:function(){var a=this.getAttribute("assetpath")||"",b=new URL(a,this.ownerDocument.baseURI);this.prototype.resolvePath=function(a,c){var d=new URL(a,c||b);return d.href}}};a.api.declaration.path=b}(Polymer),function(a){function b(a,b){var c=new URL(a.getAttribute("href"),b).href;return"@import '"+c+"';"}function c(a,b){if(a){b===document&&(b=document.head),i&&(b=document.head);var c=d(a.textContent),e=a.getAttribute(h);e&&c.setAttribute(h,e);var f=b.firstElementChild;if(b===document.head){var g="style["+h+"]",j=document.head.querySelectorAll(g);j.length&&(f=j[j.length-1].nextElementSibling)}b.insertBefore(c,f)}}function d(a,b){b=b||document,b=b.createElement?b:b.ownerDocument;var c=b.createElement("style");return c.textContent=a,c}function e(a){return a&&a.__resource||""}function f(a,b){return q?q.call(a,b):void 0}var g=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.styles),h=g.STYLE_SCOPE_ATTRIBUTE,i=window.ShadowDOMPolyfill,j="style",k="@import",l="link[rel=stylesheet]",m="global",n="polymer-scope",o={loadStyles:function(a){var b=this.fetchTemplate(),c=b&&this.templateContent();if(c){this.convertSheetsToStyles(c);var d=this.findLoadableStyles(c);if(d.length){var e=b.ownerDocument.baseURI;return Polymer.styleResolver.loadStyles(d,e,a)}}a&&a()},convertSheetsToStyles:function(a){for(var c,e,f=a.querySelectorAll(l),g=0,h=f.length;h>g&&(c=f[g]);g++)e=d(b(c,this.ownerDocument.baseURI),this.ownerDocument),this.copySheetAttributes(e,c),c.parentNode.replaceChild(e,c)},copySheetAttributes:function(a,b){for(var c,d=0,e=b.attributes,f=e.length;(c=e[d])&&f>d;d++)"rel"!==c.name&&"href"!==c.name&&a.setAttribute(c.name,c.value)},findLoadableStyles:function(a){var b=[];if(a)for(var c,d=a.querySelectorAll(j),e=0,f=d.length;f>e&&(c=d[e]);e++)c.textContent.match(k)&&b.push(c);return b},installSheets:function(){this.cacheSheets(),this.cacheStyles(),this.installLocalSheets(),this.installGlobalStyles()},cacheSheets:function(){this.sheets=this.findNodes(l),this.sheets.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},cacheStyles:function(){this.styles=this.findNodes(j+"["+n+"]"),this.styles.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},installLocalSheets:function(){var a=this.sheets.filter(function(a){return!a.hasAttribute(n)}),b=this.templateContent();if(b){var c="";if(a.forEach(function(a){c+=e(a)+"\n"}),c){var f=d(c,this.ownerDocument);b.insertBefore(f,b.firstChild)}}},findNodes:function(a,b){var c=this.querySelectorAll(a).array(),d=this.templateContent();if(d){var e=d.querySelectorAll(a).array();c=c.concat(e)}return b?c.filter(b):c},installGlobalStyles:function(){var a=this.styleForScope(m);c(a,document.head)},cssTextForScope:function(a){var b="",c="["+n+"="+a+"]",d=function(a){return f(a,c)},g=this.sheets.filter(d);g.forEach(function(a){b+=e(a)+"\n\n"});var h=this.styles.filter(d);return h.forEach(function(a){b+=a.textContent+"\n\n"}),b},styleForScope:function(a){var b=this.cssTextForScope(a);return this.cssTextToScopeStyle(b,a)},cssTextToScopeStyle:function(a,b){if(a){var c=d(a);return c.setAttribute(h,this.getAttribute("name")+"-"+b),c}}},p=HTMLElement.prototype,q=p.matches||p.matchesSelector||p.webkitMatchesSelector||p.mozMatchesSelector;a.api.declaration.styles=o,a.applyStyleToScope=c}(Polymer),function(a){var b=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.events),c=b.EVENT_PREFIX,d={};["webkitAnimationStart","webkitAnimationEnd","webkitTransitionEnd","DOMFocusOut","DOMFocusIn","DOMMouseScroll"].forEach(function(a){d[a.toLowerCase()]=a});var e={parseHostEvents:function(){var a=this.prototype.eventDelegates;this.addAttributeDelegates(a)},addAttributeDelegates:function(a){for(var b,c=0;b=this.attributes[c];c++)this.hasEventPrefix(b.name)&&(a[this.removeEventPrefix(b.name)]=b.value.replace("{{","").replace("}}","").trim())},hasEventPrefix:function(a){return a&&"o"===a[0]&&"n"===a[1]&&"-"===a[2]},removeEventPrefix:function(a){return a.slice(f)},findController:function(a){for(;a.parentNode;){if(a.eventController)return a.eventController;a=a.parentNode}return a.host},getEventHandler:function(a,b,c){var d=this;return function(e){a&&a.PolymerBase||(a=d.findController(b));var f=[e,e.detail,e.currentTarget];a.dispatchMethod(a,c,f)}},prepareEventBinding:function(a,b){if(this.hasEventPrefix(b)){var c=this.removeEventPrefix(b);c=d[c]||c;var e=this;return function(b,d,f){function g(){return"{{ "+a+" }}"}var h=e.getEventHandler(void 0,d,a);return PolymerGestures.addEventListener(d,c,h),f?void 0:{open:g,discardChanges:g,close:function(){PolymerGestures.removeEventListener(d,c,h)}}}}}},f=c.length;a.api.declaration.events=e}(Polymer),function(a){var b=["attribute"],c={inferObservers:function(a){var b,c=a.observe;for(var d in a)"Changed"===d.slice(-7)&&(b=d.slice(0,-7),this.canObserveProperty(b)&&(c||(c=a.observe={}),c[b]=c[b]||d))},canObserveProperty:function(a){return b.indexOf(a)<0},explodeObservers:function(a){var b=a.observe;if(b){var c={};for(var d in b)for(var e,f=d.split(" "),g=0;e=f[g];g++)c[e]=b[d];a.observe=c}},optimizePropertyMaps:function(a){if(a.observe){var b=a._observeNames=[];for(var c in a.observe)for(var d,e=c.split(" "),f=0;d=e[f];f++)b.push(d)}if(a.publish){var b=a._publishNames=[];for(var c in a.publish)b.push(c)}if(a.computed){var b=a._computedNames=[];for(var c in a.computed)b.push(c)}},publishProperties:function(a,b){var c=a.publish;c&&(this.requireProperties(c,a,b),this.filterInvalidAccessorNames(c),a._publishLC=this.lowerCaseMap(c));var d=a.computed;d&&this.filterInvalidAccessorNames(d)},filterInvalidAccessorNames:function(a){for(var b in a)this.propertyNameBlacklist[b]&&(console.warn('Cannot define property "'+b+'" for element "'+this.name+'" because it has the same name as an HTMLElement property, and not all browsers support overriding that. Consider giving it a different name.'),delete a[b])},requireProperties:function(a,b){b.reflect=b.reflect||{};for(var c in a){var d=a[c];d&&void 0!==d.reflect&&(b.reflect[c]=Boolean(d.reflect),d=d.value),void 0!==d&&(b[c]=d)}},lowerCaseMap:function(a){var b={};for(var c in a)b[c.toLowerCase()]=c;return b},createPropertyAccessor:function(a,b){var c=this.prototype,d=a+"_",e=a+"Observable_";c[d]=c[a],Object.defineProperty(c,a,{get:function(){var a=this[e];return a&&a.deliver(),this[d]},set:function(c){if(b)return this[d];var f=this[e];if(f)return void f.setValue(c);var g=this[d];return this[d]=c,this.emitPropertyChangeRecord(a,c,g),c},configurable:!0})},createPropertyAccessors:function(a){var b=a._computedNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)this.createPropertyAccessor(c,!0);var b=a._publishNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)a.computed&&a.computed[c]||this.createPropertyAccessor(c)},propertyNameBlacklist:{children:1,"class":1,id:1,hidden:1,style:1,title:1}};a.api.declaration.properties=c}(Polymer),function(a){var b="attributes",c=/\s|,/,d={inheritAttributesObjects:function(a){this.inheritObject(a,"publishLC"),this.inheritObject(a,"_instanceAttributes")},publishAttributes:function(a){var d=this.getAttribute(b);if(d)for(var e,f=a.publish||(a.publish={}),g=d.split(c),h=0,i=g.length;i>h;h++)e=g[h].trim(),e&&void 0===f[e]&&(f[e]=void 0)},accumulateInstanceAttributes:function(){for(var a,b=this.prototype._instanceAttributes,c=this.attributes,d=0,e=c.length;e>d&&(a=c[d]);d++)this.isInstanceAttribute(a.name)&&(b[a.name]=a.value)},isInstanceAttribute:function(a){return!this.blackList[a]&&"on-"!==a.slice(0,3)},blackList:{name:1,"extends":1,constructor:1,noscript:1,assetpath:1,"cache-csstext":1}};d.blackList[b]=1,a.api.declaration.attributes=d}(Polymer),function(a){var b=a.api.declaration.events,c=new PolymerExpressions,d=c.prepareBinding;c.prepareBinding=function(a,e,f){return b.prepareEventBinding(a,e,f)||d.call(c,a,e,f)};var e={syntax:c,fetchTemplate:function(){return this.querySelector("template")},templateContent:function(){var a=this.fetchTemplate();return a&&a.content},installBindingDelegate:function(a){a&&(a.bindingDelegate=this.syntax)}};a.api.declaration.mdv=e}(Polymer),function(a){function b(a){if(!Object.__proto__){var b=Object.getPrototypeOf(a);a.__proto__=b,d(b)&&(b.__proto__=Object.getPrototypeOf(b))}}var c=a.api,d=a.isBase,e=a.extend,f=window.ShadowDOMPolyfill,g={register:function(a,b){this.buildPrototype(a,b),this.registerPrototype(a,b),this.publishConstructor()},buildPrototype:function(b,c){var d=a.getRegisteredPrototype(b),e=this.generateBasePrototype(c);this.desugarBeforeChaining(d,e),this.prototype=this.chainPrototypes(d,e),this.desugarAfterChaining(b,c)},desugarBeforeChaining:function(a,b){a.element=this,this.publishAttributes(a,b),this.publishProperties(a,b),this.inferObservers(a),this.explodeObservers(a)},chainPrototypes:function(a,c){this.inheritMetaData(a,c);var d=this.chainObject(a,c);return b(d),d},inheritMetaData:function(a,b){this.inheritObject("observe",a,b),this.inheritObject("publish",a,b),this.inheritObject("reflect",a,b),this.inheritObject("_publishLC",a,b),this.inheritObject("_instanceAttributes",a,b),this.inheritObject("eventDelegates",a,b)},desugarAfterChaining:function(a,b){this.optimizePropertyMaps(this.prototype),this.createPropertyAccessors(this.prototype),this.installBindingDelegate(this.fetchTemplate()),this.installSheets(),this.resolveElementPaths(this),this.accumulateInstanceAttributes(),this.parseHostEvents(),this.addResolvePathApi(),f&&WebComponents.ShadowCSS.shimStyling(this.templateContent(),a,b),this.prototype.registerCallback&&this.prototype.registerCallback(this)},publishConstructor:function(){var a=this.getAttribute("constructor");a&&(window[a]=this.ctor)},generateBasePrototype:function(a){var b=this.findBasePrototype(a);if(!b){var b=HTMLElement.getPrototypeForTag(a);b=this.ensureBaseApi(b),h[a]=b}return b},findBasePrototype:function(a){return h[a]},ensureBaseApi:function(a){if(a.PolymerBase)return a;var b=Object.create(a);return c.publish(c.instance,b),this.mixinMethod(b,a,c.instance.mdv,"bind"),b},mixinMethod:function(a,b,c,d){var e=function(a){return b[d].apply(this,a)};a[d]=function(){return this.mixinSuper=e,c[d].apply(this,arguments)}},inheritObject:function(a,b,c){var d=b[a]||{};b[a]=this.chainObject(d,c[a])},registerPrototype:function(a,b){var c={prototype:this.prototype},d=this.findTypeExtension(b);d&&(c["extends"]=d),HTMLElement.register(a,this.prototype),this.ctor=document.registerElement(a,c)},findTypeExtension:function(a){if(a&&a.indexOf("-")<0)return a;var b=this.findBasePrototype(a);return b.element?this.findTypeExtension(b.element["extends"]):void 0}},h={};g.chainObject=Object.__proto__?function(a,b){return a&&b&&a!==b&&(a.__proto__=b),a}:function(a,b){if(a&&b&&a!==b){var c=Object.create(b);a=e(c,a)}return a},c.declaration.prototype=g}(Polymer),function(a){function b(a){return document.contains(a)?j:i}function c(){return i.length?i[0]:j[0]}function d(a){f.waitToReady=!0,Polymer.endOfMicrotask(function(){HTMLImports.whenReady(function(){f.addReadyCallback(a),f.waitToReady=!1,f.check()})})}function e(a){if(void 0===a)return void f.ready();var b=setTimeout(function(){f.ready()},a);Polymer.whenReady(function(){clearTimeout(b)})}var f={wait:function(a){a.__queue||(a.__queue={},g.push(a))},enqueue:function(a,c,d){var e=a.__queue&&!a.__queue.check;return e&&(b(a).push(a),a.__queue.check=c,a.__queue.go=d),0!==this.indexOf(a)},indexOf:function(a){var c=b(a).indexOf(a);return c>=0&&document.contains(a)&&(c+=HTMLImports.useNative||HTMLImports.ready?i.length:1e9),c},go:function(a){var b=this.remove(a);b&&(a.__queue.flushable=!0,this.addToFlushQueue(b),this.check())},remove:function(a){var c=this.indexOf(a);if(0===c)return b(a).shift()},check:function(){var a=this.nextElement();return a&&a.__queue.check.call(a),this.canReady()?(this.ready(),!0):void 0},nextElement:function(){return c()},canReady:function(){return!this.waitToReady&&this.isEmpty()},isEmpty:function(){for(var a,b=0,c=g.length;c>b&&(a=g[b]);b++)if(a.__queue&&!a.__queue.flushable)return;return!0},addToFlushQueue:function(a){h.push(a)},flush:function(){if(!this.flushing){this.flushing=!0;for(var a;h.length;)a=h.shift(),a.__queue.go.call(a),a.__queue=null;this.flushing=!1}},ready:function(){var a=CustomElements.ready;CustomElements.ready=!1,this.flush(),CustomElements.useNative||CustomElements.upgradeDocumentTree(document),CustomElements.ready=a,Polymer.flush(),requestAnimationFrame(this.flushReadyCallbacks)},addReadyCallback:function(a){a&&k.push(a)},flushReadyCallbacks:function(){if(k)for(var a;k.length;)(a=k.shift())()},waitingFor:function(){for(var a,b=[],c=0,d=g.length;d>c&&(a=g[c]);c++)a.__queue&&!a.__queue.flushable&&b.push(a);return b},waitToReady:!0},g=[],h=[],i=[],j=[],k=[];a.elements=g,a.waitingFor=f.waitingFor.bind(f),a.forceReady=e,a.queue=f,a.whenReady=a.whenPolymerReady=d}(Polymer),function(a){function b(a){return Boolean(HTMLElement.getPrototypeForTag(a))}function c(a){return a&&a.indexOf("-")>=0}var d=a.extend,e=a.api,f=a.queue,g=a.whenReady,h=a.getRegisteredPrototype,i=a.waitingForPrototype,j=d(Object.create(HTMLElement.prototype),{createdCallback:function(){this.getAttribute("name")&&this.init()},init:function(){this.name=this.getAttribute("name"),this["extends"]=this.getAttribute("extends"),f.wait(this),this.loadResources(),this.registerWhenReady()},registerWhenReady:function(){this.registered||this.waitingForPrototype(this.name)||this.waitingForQueue()||this.waitingForResources()||f.go(this)},_register:function(){c(this["extends"])&&!b(this["extends"])&&console.warn("%s is attempting to extend %s, an unregistered element or one that was not registered with Polymer.",this.name,this["extends"]),this.register(this.name,this["extends"]),this.registered=!0},waitingForPrototype:function(a){return h(a)?void 0:(i(a,this),this.handleNoScript(a),!0)},handleNoScript:function(a){this.hasAttribute("noscript")&&!this.noscript&&(this.noscript=!0,Polymer(a))},waitingForResources:function(){return this._needsResources},waitingForQueue:function(){return f.enqueue(this,this.registerWhenReady,this._register)},loadResources:function(){this._needsResources=!0,this.loadStyles(function(){this._needsResources=!1,this.registerWhenReady()}.bind(this))}});e.publish(e.declaration,j),g(function(){document.body.removeAttribute("unresolved"),document.dispatchEvent(new CustomEvent("polymer-ready",{bubbles:!0}))}),document.registerElement("polymer-element",{prototype:j})}(Polymer),function(a){function b(a,b){a?(document.head.appendChild(a),d(b)):b&&b()}function c(a,c){if(a&&a.length){for(var d,e,f=document.createDocumentFragment(),g=0,h=a.length;h>g&&(d=a[g]);g++)e=document.createElement("link"),e.rel="import",e.href=d,f.appendChild(e);b(f,c)}else c&&c()}var d=a.whenReady;a["import"]=c,a.importElements=b}(Polymer),function(){var a=document.createElement("polymer-element");a.setAttribute("name","auto-binding"),a.setAttribute("extends","template"),a.init(),Polymer("auto-binding",{createdCallback:function(){this.syntax=this.bindingDelegate=this.makeSyntax(),Polymer.whenPolymerReady(function(){this.model=this,this.setAttribute("bind",""),this.async(function(){this.marshalNodeReferences(this.parentNode),this.fire("template-bound")})}.bind(this))},makeSyntax:function(){var a=Object.create(Polymer.api.declaration.events),b=this;a.findController=function(){return b.model};var c=new PolymerExpressions,d=c.prepareBinding;return c.prepareBinding=function(b,e,f){return a.prepareEventBinding(b,e,f)||d.call(c,b,e,f)},c}})}(); - diff --git a/homeassistant/components/frontend/www_static/polymer/components/display-time.html b/homeassistant/components/frontend/www_static/polymer/components/display-time.html new file mode 100644 index 00000000000..ff2f0a6dd8f --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/display-time.html @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html new file mode 100644 index 00000000000..4b6177a6949 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html new file mode 100644 index 00000000000..e454fee9ed7 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant-js b/homeassistant/components/frontend/www_static/polymer/home-assistant-js index e048bf6ece9..282004e3e27 160000 --- a/homeassistant/components/frontend/www_static/polymer/home-assistant-js +++ b/homeassistant/components/frontend/www_static/polymer/home-assistant-js @@ -1 +1 @@ -Subproject commit e048bf6ece91983b9f03aafeb414ae5c535288a2 +Subproject commit 282004e3e27134a3de1b9c0e6c264ce811f3e510 diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html index ade9c9d166b..cee51c78998 100644 --- a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html +++ b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html @@ -10,6 +10,7 @@ + @@ -96,6 +97,13 @@ + +
@@ -136,6 +144,9 @@ + @@ -161,6 +172,7 @@ Polymer(Polymer.mixin({ narrow: false, activeFilters: [], hasHistoryComponent: false, + hasLogbookComponent: false, isStreaming: false, hasStreamError: false, @@ -185,7 +197,7 @@ Polymer(Polymer.mixin({ componentStoreChanged: function(componentStore) { this.hasHistoryComponent = componentStore.isLoaded('history'); - this.hasScriptComponent = componentStore.isLoaded('script'); + this.hasLogbookComponent = componentStore.isLoaded('logbook'); }, streamStoreChanged: function(streamStore) { diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html new file mode 100644 index 00000000000..faa7f93fea1 --- /dev/null +++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-logbook.html @@ -0,0 +1,56 @@ + + + + + + + + + + diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html index 2d8b6d6e536..0567c7a5300 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html @@ -51,7 +51,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) { case "media_player": var icon = "hardware:cast"; - if (state !== "idle") { + if (state && state !== "idle") { icon += "-connected"; } diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html index 17049015294..94abddfabb7 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html @@ -1,5 +1,30 @@ + +/* Palette generated by Material Palette - materialpalette.com/light-blue/orange */ + +.dark-primary-color { background: #0288D1; } +.default-primary-color { background: #03A9F4; } +.light-primary-color { background: #B3E5FC; } +.text-primary-color { color: #FFFFFF; } +.accent-color { background: #FF9800; } +.primary-text-color { color: #212121; } +.secondary-text-color { color: #727272; } +.divider-color { border-color: #B6B6B6; } + +/* extra */ +.accent-text-colo { color: #FF9800; } + +body { + color: #212121; +} + +a { + color: #FF9800; + text-decoration: none; +} + + @-webkit-keyframes ha-spin { 0% { From 9fb634ed3aeca8aba900da41ba6540a0f9c1425f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 15:23:50 -0700 Subject: [PATCH 4/7] Fix type in CSS class name --- .../www_static/polymer/resources/home-assistant-style.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html index 94abddfabb7..cb0dbe8e414 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-style.html @@ -13,7 +13,7 @@ .divider-color { border-color: #B6B6B6; } /* extra */ -.accent-text-colo { color: #FF9800; } +.accent-text-color { color: #FF9800; } body { color: #212121; From 6455f1388ae48d0adfe33edd96b569f112a21f4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Mar 2015 23:57:52 -0700 Subject: [PATCH 5/7] Have logbook only report each sensor every 15 minutes --- homeassistant/components/logbook.py | 115 ++++++++++++++++++---------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index b5739638a43..8cf750fb932 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -5,6 +5,7 @@ homeassistant.components.logbook Parses events and generates a human log """ from datetime import datetime +from itertools import groupby from homeassistant import State, DOMAIN as HA_DOMAIN from homeassistant.const import ( @@ -25,6 +26,8 @@ QUERY_EVENTS_BETWEEN = """ ORDER BY time_fired """ +GROUP_BY_MINUTES = 15 + def setup(hass, config): """ Listens for download events to download files. """ @@ -72,56 +75,86 @@ class Entry(object): def humanify(events): - """ Generator that converts a list of events into Entry objects. """ + """ + Generator that converts a list of events into Entry objects. + + Will try to group events if possible: + - if 2+ sensor updates in GROUP_BY_MINUTES, show last + """ # pylint: disable=too-many-branches - for event in events: - if event.event_type == EVENT_STATE_CHANGED: - # Do not report on new entities - if 'old_state' not in event.data: - continue + # Group events in batches of GROUP_BY_MINUTES + for _, g_events in groupby( + events, + lambda event: event.time_fired.minute // GROUP_BY_MINUTES): - to_state = State.from_dict(event.data.get('new_state')) + events_batch = list(g_events) - if not to_state: - continue + # Keep track of last sensor states + last_sensor_event = {} - domain = to_state.domain + # Process events + for event in events_batch: + if event.event_type == EVENT_STATE_CHANGED: + entity_id = event.data['entity_id'] - entry = Entry( - event.time_fired, domain=domain, - name=to_state.name, entity_id=to_state.entity_id) + if entity_id.startswith('sensor.'): + last_sensor_event[entity_id] = event - if domain == 'device_tracker': - entry.message = '{} home'.format( - 'arrived' if to_state.state == STATE_HOME else 'left') + # Yield entries + for event in events_batch: + if event.event_type == EVENT_STATE_CHANGED: + + # Do not report on new entities + if 'old_state' not in event.data: + continue + + to_state = State.from_dict(event.data.get('new_state')) + + if not to_state: + continue + + domain = to_state.domain + + # Skip all but the last sensor state + if domain == 'sensor' and \ + event != last_sensor_event[to_state.entity_id]: + continue + + entry = Entry( + event.time_fired, domain=domain, + name=to_state.name, entity_id=to_state.entity_id) + + if domain == 'device_tracker': + entry.message = '{} home'.format( + 'arrived' if to_state.state == STATE_HOME else 'left') + + elif domain == 'sun': + if to_state.state == sun.STATE_ABOVE_HORIZON: + entry.message = 'has risen' + else: + entry.message = 'has set' + + elif to_state.state == STATE_ON: + # Future: combine groups and its entity entries ? + entry.message = "turned on" + + elif to_state.state == STATE_OFF: + entry.message = "turned off" - elif domain == 'sun': - if to_state.state == sun.STATE_ABOVE_HORIZON: - entry.message = 'has risen' else: - entry.message = 'has set' + entry.message = "changed to {}".format(to_state.state) - elif to_state.state == STATE_ON: - # Future: combine groups and its entity entries ? - entry.message = "turned on" + if entry.is_valid: + yield entry - elif to_state.state == STATE_OFF: - entry.message = "turned off" + elif event.event_type == EVENT_HOMEASSISTANT_START: + # Future: look for sequence stop/start and rewrite as restarted + yield Entry( + event.time_fired, "Home Assistant", "started", + domain=HA_DOMAIN) - else: - entry.message = "changed to {}".format(to_state.state) - - if entry.is_valid: - yield entry - - elif event.event_type == EVENT_HOMEASSISTANT_START: - # Future: look for sequence stop/start and rewrite as restarted - yield Entry( - event.time_fired, "Home Assistant", "started", - domain=HA_DOMAIN) - - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - yield Entry( - event.time_fired, "Home Assistant", "stopped", - domain=HA_DOMAIN) + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + yield Entry( + event.time_fired, "Home Assistant", "stopped", + domain=HA_DOMAIN) From 742479f8bd9f035add48483da3658d0a3ca5cca0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Mar 2015 00:11:24 -0700 Subject: [PATCH 6/7] Have logbook group HA stop + start --- homeassistant/components/logbook.py | 30 ++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 8cf750fb932..dd7d3275d84 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -80,8 +80,9 @@ def humanify(events): Will try to group events if possible: - if 2+ sensor updates in GROUP_BY_MINUTES, show last + - if home assistant stop and start happen in same minute call it restarted """ - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches, too-many-statements # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -93,6 +94,10 @@ def humanify(events): # Keep track of last sensor states last_sensor_event = {} + # group HA start/stop events + # Maps minute of event to 1: stop, 2: stop + start + start_stop_events = {} + # Process events for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: @@ -101,6 +106,18 @@ def humanify(events): if entity_id.startswith('sensor.'): last_sensor_event[entity_id] = event + elif event.event_type == EVENT_HOMEASSISTANT_STOP: + if event.time_fired.minute in start_stop_events: + continue + + start_stop_events[event.time_fired.minute] = 1 + + elif event.event_type == EVENT_HOMEASSISTANT_START: + if event.time_fired.minute not in start_stop_events: + continue + + start_stop_events[event.time_fired.minute] = 2 + # Yield entries for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: @@ -149,12 +166,19 @@ def humanify(events): yield entry elif event.event_type == EVENT_HOMEASSISTANT_START: - # Future: look for sequence stop/start and rewrite as restarted + if start_stop_events.get(event.time_fired.minute) == 2: + continue + yield Entry( event.time_fired, "Home Assistant", "started", domain=HA_DOMAIN) elif event.event_type == EVENT_HOMEASSISTANT_STOP: + if start_stop_events.get(event.time_fired.minute) == 2: + action = "restarted" + else: + action = "stopped" + yield Entry( - event.time_fired, "Home Assistant", "stopped", + event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) From 30e7f09000c78039114caca50658588bf80ecb62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Mar 2015 00:19:56 -0700 Subject: [PATCH 7/7] Clean up logbook component --- homeassistant/components/logbook.py | 65 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index dd7d3275d84..810b06b25da 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -48,7 +48,7 @@ def _handle_get_logbook(handler, path_match, data): class Entry(object): """ A human readable version of the log. """ - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-few-public-methods def __init__(self, when=None, name=None, message=None, domain=None, entity_id=None): @@ -58,11 +58,6 @@ class Entry(object): self.domain = domain self.entity_id = entity_id - @property - def is_valid(self): - """ Returns if this entry contains all the needed fields. """ - return self.when and self.name and self.message - def as_dict(self): """ Convert Entry to a dict to be used within JSON. """ return { @@ -82,7 +77,7 @@ def humanify(events): - if 2+ sensor updates in GROUP_BY_MINUTES, show last - if home assistant stop and start happen in same minute call it restarted """ - # pylint: disable=too-many-branches, too-many-statements + # pylint: disable=too-many-branches # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -138,32 +133,12 @@ def humanify(events): event != last_sensor_event[to_state.entity_id]: continue - entry = Entry( - event.time_fired, domain=domain, - name=to_state.name, entity_id=to_state.entity_id) - - if domain == 'device_tracker': - entry.message = '{} home'.format( - 'arrived' if to_state.state == STATE_HOME else 'left') - - elif domain == 'sun': - if to_state.state == sun.STATE_ABOVE_HORIZON: - entry.message = 'has risen' - else: - entry.message = 'has set' - - elif to_state.state == STATE_ON: - # Future: combine groups and its entity entries ? - entry.message = "turned on" - - elif to_state.state == STATE_OFF: - entry.message = "turned off" - - else: - entry.message = "changed to {}".format(to_state.state) - - if entry.is_valid: - yield entry + yield Entry( + event.time_fired, + name=to_state.name, + message=_entry_message_from_state(domain, to_state), + domain=domain, + entity_id=to_state.entity_id) elif event.event_type == EVENT_HOMEASSISTANT_START: if start_stop_events.get(event.time_fired.minute) == 2: @@ -182,3 +157,27 @@ def humanify(events): yield Entry( event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) + + +def _entry_message_from_state(domain, state): + """ Convert a state to a message for the logbook. """ + # We pass domain in so we don't have to split entity_id again + + if domain == 'device_tracker': + return '{} home'.format( + 'arrived' if state.state == STATE_HOME else 'left') + + elif domain == 'sun': + if state.state == sun.STATE_ABOVE_HORIZON: + return 'has risen' + else: + return 'has set' + + elif state.state == STATE_ON: + # Future: combine groups and its entity entries ? + return "turned on" + + elif state.state == STATE_OFF: + return "turned off" + + return "changed to {}".format(state.state)