From aec861c1a4a44b0d90aba4608b238b2abeca1573 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Mar 2015 12:38:30 -0700 Subject: [PATCH 001/842] Fixes for new release PyLint --- homeassistant/components/light/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 6e3bb7667f0..a0bcd742aa2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -190,7 +190,6 @@ def setup(hass, config): if service.service == SERVICE_TURN_OFF: for light in target_lights: - # pylint: disable=star-args light.turn_off(**params) else: @@ -248,7 +247,6 @@ def setup(hass, config): params[ATTR_FLASH] = FLASH_LONG for light in target_lights: - # pylint: disable=star-args light.turn_on(**params) for light in target_lights: From 6da0257bb66150dfd75af902dfb519d5c875a4da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Mar 2015 19:13:03 -0700 Subject: [PATCH 002/842] Fix a config bug in Automation --- config/configuration.yaml.example | 3 ++- homeassistant/components/automation/__init__.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 51217c090bb..c758833d336 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -101,7 +101,8 @@ automation 2: time_seconds: 0 execute_service: notify.notify - service_data: {"message":"It's 4, time for beer!"} + service_data: + message: It's 4, time for beer! sensor: platform: systemmonitor diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 33eef9fe3dc..6184d53ebf6 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -54,8 +54,7 @@ def _get_action(hass, config): if CONF_SERVICE in config: domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = convert( - config.get(CONF_SERVICE_DATA), json.loads, {}) + service_data = config.get(CONF_SERVICE_DATA, {}) if not isinstance(service_data, dict): _LOGGER.error( From 60fbc51a2daf503c36baa462a1d3c1c138844603 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 22 Mar 2015 06:10:24 +1100 Subject: [PATCH 003/842] added in line graph support for state history --- .../polymer/components/state-timeline.html | 188 ++++++++++++++---- 1 file changed, 146 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/components/state-timeline.html b/homeassistant/components/frontend/www_static/polymer/components/state-timeline.html index 9ded10dd3ae..997a8c2e528 100644 --- a/homeassistant/components/frontend/www_static/polymer/components/state-timeline.html +++ b/homeassistant/components/frontend/www_static/polymer/components/state-timeline.html @@ -12,16 +12,17 @@
- +
- 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 010/842] 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 011/842] 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 012/842] 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 013/842] 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) From 00bbc17e11753cd10f57f07e437aab6f22f87f70 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Mar 2015 23:08:38 -0700 Subject: [PATCH 014/842] Add State.last_updated to JSON obj --- homeassistant/__init__.py | 15 +++++++++++---- homeassistant/components/recorder.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index d1cb17f22c6..898b66c4ef9 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -463,7 +463,8 @@ class State(object): __slots__ = ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated'] - def __init__(self, entity_id, state, attributes=None, last_changed=None): + def __init__(self, entity_id, state, attributes=None, last_changed=None, + last_updated=None): if not ENTITY_ID_PATTERN.match(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " @@ -472,7 +473,7 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = dt.datetime.now() + self.last_updated = last_updated or dt.datetime.now() # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) @@ -510,7 +511,8 @@ class State(object): return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, - 'last_changed': util.datetime_to_str(self.last_changed)} + 'last_changed': util.datetime_to_str(self.last_changed), + 'last_updated': util.datetime_to_str(self.last_updated)} @classmethod def from_dict(cls, json_dict): @@ -527,8 +529,13 @@ class State(object): if last_changed: last_changed = util.str_to_datetime(last_changed) + last_updated = json_dict.get('last_updated') + + if last_updated: + last_updated = util.str_to_datetime(last_updated) + return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed) + json_dict.get('attributes'), last_changed, last_updated) def __eq__(self, other): return (self.__class__ == other.__class__ and diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index f2e5fa35ad4..6856ce4d7b5 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -60,7 +60,8 @@ 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])) + row[1], row[2], json.loads(row[3]), datetime.fromtimestamp(row[4]), + datetime.fromtimestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) From 57b3e8018b1d8e1829df62b5688bd1ebb4b741ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Mar 2015 23:09:08 -0700 Subject: [PATCH 015/842] Logbook bug fixes --- .../www_static/polymer/components/logbook-entry.html | 3 ++- homeassistant/components/logbook.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html index e454fee9ed7..6d5bd917fb5 100644 --- a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html +++ b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html @@ -50,7 +50,8 @@ var uiActions = window.hass.uiActions; Polymer({ - entityClicked: function() { + entityClicked: function(ev) { + ev.preventDefault(); uiActions.showMoreInfoDialog(this.entryObj.entityId); } }); diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 810b06b25da..3b42ffdee57 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -39,8 +39,7 @@ def setup(hass, config): 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,)))) @@ -123,7 +122,7 @@ def humanify(events): to_state = State.from_dict(event.data.get('new_state')) - if not to_state: + if not to_state or to_state.last_changed != to_state.last_updated: continue domain = to_state.domain From e43eee2eb13a00b7e7ab4f9e190e8dfc0e9266db Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Apr 2015 07:18:03 -0700 Subject: [PATCH 016/842] Style fixes --- homeassistant/__init__.py | 1 + homeassistant/components/logbook.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 898b66c4ef9..5e54cbd5d0a 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -463,6 +463,7 @@ class State(object): __slots__ = ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated'] + # pylint: disable=too-many-arguments def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): if not ENTITY_ID_PATTERN.match(entity_id): diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 3b42ffdee57..a8299fbd6ed 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -122,7 +122,10 @@ def humanify(events): to_state = State.from_dict(event.data.get('new_state')) - if not to_state or to_state.last_changed != to_state.last_updated: + # if last_changed == last_updated only attributes have changed + # we do not report on that yet. + if not to_state or \ + to_state.last_changed != to_state.last_updated: continue domain = to_state.domain From b0bf775da8a6dc2fab6cfd1f5cf1e8fffd709416 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Apr 2015 21:49:03 -0700 Subject: [PATCH 017/842] Compile new version frontend --- homeassistant/components/frontend/version.py | 2 +- homeassistant/components/frontend/www_static/frontend.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index ea306279316..095769c9c28 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 = "b06d3667e9e461173029ded9c0c9b815" +VERSION = "1e004712440afc642a44ad927559587e" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 32b16c2fb6a..10ac2336a3c 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -17,7 +17,7 @@ b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.loc .divider-color { border-color: #B6B6B6; } /* extra */ -.accent-text-colo { color: #FF9800; } +.accent-text-color { color: #FF9800; } body { color: #212121; @@ -213,7 +213,7 @@ return pickBy("isBefore",args)};moment.max=function(){var args=[].slice.call(arg {{ time }} + + diff --git a/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html b/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html index 70e14e72d7f..14742dd2d2d 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html @@ -3,3 +3,22 @@ --> + + From 372033392732d32256c3bc4a7aa192708493998d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Apr 2015 22:38:43 -0700 Subject: [PATCH 108/842] UTC bugfixes --- homeassistant/components/logbook.py | 2 +- homeassistant/components/recorder.py | 43 +++++++++------------------- homeassistant/util/dt.py | 9 ++---- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 4d1e5ba8e00..68718a7ac43 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -37,7 +37,7 @@ def setup(hass, config): def _handle_get_logbook(handler, path_match, data): """ Return logbook entries. """ - start_today = dt_util.now().date() + start_today = dt_util.now().replace(hour=0, minute=0, second=0) handler.write_json(humanify( recorder.query_events( diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index ad4d2f3caa4..4f0a76cf18a 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -10,7 +10,6 @@ import threading import queue import sqlite3 from datetime import datetime, date -import time import json import atexit @@ -302,7 +301,7 @@ class Recorder(threading.Thread): migration_id = 0 if migration_id < 1: - cur.execute(""" + self.query(""" CREATE TABLE recorder_runs ( run_id integer primary key, start integer, @@ -311,7 +310,7 @@ class Recorder(threading.Thread): created integer) """) - cur.execute(""" + self.query(""" CREATE TABLE events ( event_id integer primary key, event_type text, @@ -319,10 +318,10 @@ class Recorder(threading.Thread): origin text, created integer) """) - cur.execute( + self.query( 'CREATE INDEX events__event_type ON events(event_type)') - cur.execute(""" + self.query(""" CREATE TABLE states ( state_id integer primary key, entity_id text, @@ -332,55 +331,41 @@ class Recorder(threading.Thread): last_updated integer, created integer) """) - cur.execute('CREATE INDEX states__entity_id ON states(entity_id)') + self.query('CREATE INDEX states__entity_id ON states(entity_id)') save_migration(1) if migration_id < 2: - cur.execute(""" + self.query(""" ALTER TABLE events ADD COLUMN time_fired integer """) - cur.execute('UPDATE events SET time_fired=created') + self.query('UPDATE events SET time_fired=created') save_migration(2) if migration_id < 3: utc_offset = self.utc_offset - cur.execute(""" + self.query(""" ALTER TABLE recorder_runs ADD COLUMN utc_offset integer """) - cur.execute(""" + self.query(""" ALTER TABLE events ADD COLUMN utc_offset integer """) - cur.execute(""" + self.query(""" ALTER TABLE states ADD COLUMN utc_offset integer """) - cur.execute("UPDATE schema_version SET performed=performed+?", - (utc_offset,)) - - cur.execute(""" - UPDATE recorder_runs SET utc_offset=?, - start=start + ?, end=end + ?, created=created + ? - """, [utc_offset]*4) - - cur.execute(""" - UPDATE events SET utc_offset=?, - time_fired=time_fired + ?, created=created + ? - """, [utc_offset]*3) - - cur.execute(""" - UPDATE states SET utc_offset=?, last_changed=last_changed + ?, - last_updated=last_updated + ?, created=created + ? - """, [utc_offset]*4) + self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset]) + self.query("UPDATE events SET utc_offset=?", [utc_offset]) + self.query("UPDATE states SET utc_offset=?", [utc_offset]) save_migration(3) @@ -411,7 +396,7 @@ class Recorder(threading.Thread): def _adapt_datetime(datetimestamp): """ Turn a datetime into an integer for in the DB. """ - return time.mktime(date_util.as_utc(datetimestamp).timetuple()) + return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() def _verify_instance(): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index c0e695bbb0c..2439f77c558 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -32,15 +32,12 @@ def get_time_zone(time_zone_str): def utcnow(): """ Get now in UTC time. """ - return dt.datetime.utcnow().replace(tzinfo=pytz.utc) + return dt.datetime.now(pytz.utc) def now(time_zone=None): """ Get now in specified time zone. """ - if time_zone is None: - time_zone = DEFAULT_TIME_ZONE - - return utcnow().astimezone(time_zone) + return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE) def as_utc(dattim): @@ -66,7 +63,7 @@ def as_local(dattim): def utc_from_timestamp(timestamp): """ Returns a UTC time from a timestamp. """ - return dt.datetime.fromtimestamp(timestamp, pytz.utc) + return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc) def datetime_to_local_str(dattim, time_zone=None): From 10a5db7924d3c7bdc8bee451a9d503d0b5144567 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Apr 2015 23:50:53 -0700 Subject: [PATCH 109/842] UTC bugfix for more-info-sun --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 2 +- .../polymer/more-infos/more-info-sun.html | 21 ++++++++++++------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 9c13c6d75a2..30b4dbae681 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 = "37514744ac03f1d764a70b8e6a7e572f" +VERSION = "fdfcc1c10ff8713976c482931769a8e6" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 3835c812b98..f9812693475 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -18,4 +18,4 @@ for(var n=-1,r=t.length,i=-1,o=[];++n>>1,Ou=au?au.BYTES_PER_ELEMENT:0,Iu=Co.pow(2,53)-1,Su=uu&&new uu,Au={},Nu=e.support={};!function(t){var e=function(){this.x=t},n=[];e.prototype={valueOf:t,y:t};for(var r in new e)n.push(r);Nu.funcDecomp=/\bthis\b/.test(function(){return this}),Nu.funcNames="string"==typeof ko.name;try{Nu.dom=11===qo.createDocumentFragment().nodeType}catch(i){Nu.dom=!1}try{Nu.nonEnumArgs=!eu.call(arguments,1)}catch(i){Nu.nonEnumArgs=!0}}(1,0),e.templateSettings={escape:Et,evaluate:Ot,interpolate:It,variable:"",imports:{_:e}};var ku=su||function(t,e){return null==e?t:ge(e,Uu(e),ge(e,ja(e),t))},Cu=function(){function e(){}return function(n){if(Ti(n)){e.prototype=n;var r=new e;e.prototype=null}return r||t.Object()}}(),Du=fn(ke),xu=fn(Ce,!0),Mu=ln(),Ru=ln(!0),ju=Su?function(t,e){return Su.set(t,e),t}:ho;Fo||(on=Ho&&ou?function(t){var e=t.byteLength,n=au?Yo(e/Ou):0,r=n*Ou,i=new Ho(e);if(n){var o=new au(i,0,n);o.set(new au(t,0,n))}return e!=r&&(o=new ou(i,r),o.set(new ou(t,r))),i}:lo(null));var Lu=fu&&nu?function(t){return new Xt(t)}:lo(null),zu=Su?function(t){return Su.get(t)}:go,Pu=function(){return Nu.funcNames?"constant"==lo.name?We("name"):function(t){for(var e=t.name,n=Au[e],r=n?n.length:0;r--;){var i=n[r],o=i.func;if(null==o||o==t)return i.name}return e}:lo("")}(),qu=We("length"),Uu=Xo?function(t){return Xo(nr(t))}:lo([]),Wu=function(){var t=0,e=0;return function(n,r){var i=fa(),o=W-(i-e);if(e=i,o>0){if(++t>=U)return n}else t=0;return ju(n,r)}}(),Vu=si(function(t,e){return Ea(t)||_i(t)?be(t,Ae(e,!1,!0)):[]}),Gu=gn(),Ku=gn(!0),$u=si(function(t,e){t||(t=[]),e=Ae(e);var n=ye(t,e);return Ge(t,e.sort(o)),n}),Hu=Cn(),Fu=Cn(!0),Bu=si(function(t){return Xe(Ae(t,!1,!0))}),Ju=si(function(t,e){return Ea(t)||_i(t)?be(t,e):[]}),Yu=si(kr),Xu=si(function(t,e){var n=t?qu(t):0;return Fn(n)&&(t=er(t)),ye(t,Ae(e))}),Zu=sn(function(t,e,n){Wo.call(t,n)?++t[n]:t[n]=1}),Qu=yn(Du),ta=yn(xu,!0),ea=Tn(ee,Du),na=Tn(ne,xu),ra=sn(function(t,e,n){Wo.call(t,n)?t[n].push(e):t[n]=[e]}),ia=sn(function(t,e,n){t[n]=e}),oa=si(function(t,e,n){var r=-1,i="function"==typeof e,o=$n(e),u=qu(t),a=Fn(u)?So(u):[];return Du(t,function(t){var u=i?e:o&&null!=t&&t[e];a[++r]=u?u.apply(t,n):Vn(t,e,n)}),a}),ua=sn(function(t,e,n){t[n?0:1].push(e)},function(){return[[],[]]}),aa=Sn(fe,Du),sa=Sn(le,xu),ca=si(function(t,e){if(null==t)return[];var n=e[2];return n&&Kn(e[0],e[1],n)&&(e.length=1),Je(t,Ae(e),[])}),fa=vu||function(){return(new Ao).getTime()},la=si(function(t,e,n){var r=k;if(n.length){var i=T(n,la.placeholder);r|=R}return Dn(t,r,e,n,i)}),ha=si(function(t,e){e=e.length?Ae(e):Ri(t);for(var n=-1,r=e.length;++nt?this.takeRight(-t):this.drop(t);return e!==A&&(e=+e||0,n=0>e?n.dropRight(-e):n.take(e-t)),n},i.prototype.toArray=function(){return this.drop(0)},ke(i.prototype,function(t,n){var o=e[n];if(o){var u=/^(?:filter|map|reject)|While$/.test(n),a=/^(?:first|last)$/.test(n);e.prototype[n]=function(){var n=arguments,s=(n.length,this.__chain__),c=this.__wrapped__,f=!!this.__actions__.length,l=c instanceof i,h=n[0],p=l||Ea(c);p&&u&&"function"==typeof h&&1!=h.length&&(l=p=!1);var _=l&&!f;if(a&&!s)return _?t.call(c):o.call(e,this.value());var v=function(t){var r=[t];return Qo.apply(r,n),o.apply(e,r)};if(p){var d=_?c:new i(this),y=t.apply(d,n);if(!a&&(f||y.__actions__)){var g=y.__actions__||(y.__actions__=[]);g.push({func:Rr,args:[v],thisArg:e})}return new r(y,s)}return this.thru(v)}}}),ee(["concat","join","pop","push","replace","shift","sort","splice","split","unshift"],function(t){var n=(/^(?:replace|split)$/.test(t)?Po:Lo)[t],r=/^(?:push|sort|unshift)$/.test(t)?"tap":"thru",i=/^(?:join|pop|replace|shift)$/.test(t);e.prototype[t]=function(){var t=arguments;return i&&!this.__chain__?n.apply(this.value(),t):this[r](function(e){return n.apply(e,t)})}}),ke(i.prototype,function(t,n){var r=e[n];if(r){var i=r.name,o=Au[i]||(Au[i]=[]);o.push({name:n,func:r})}}),Au[An(null,C).name]=[{name:"wrapper",func:null}],i.prototype.clone=w,i.prototype.reverse=Q,i.prototype.value=rt,e.prototype.chain=jr,e.prototype.commit=Lr,e.prototype.plant=zr,e.prototype.reverse=Pr,e.prototype.toString=qr,e.prototype.run=e.prototype.toJSON=e.prototype.valueOf=e.prototype.value=Ur,e.prototype.collect=e.prototype.map,e.prototype.head=e.prototype.first,e.prototype.select=e.prototype.filter,e.prototype.tail=e.prototype.rest,e}var A,N="3.7.0",k=1,C=2,D=4,x=8,M=16,R=32,j=64,L=128,z=256,P=30,q="...",U=150,W=16,V=0,G=1,K=2,$="Expected a function",H="__lodash_placeholder__",F="[object Arguments]",B="[object Array]",J="[object Boolean]",Y="[object Date]",X="[object Error]",Z="[object Function]",Q="[object Map]",tt="[object Number]",et="[object Object]",nt="[object RegExp]",rt="[object Set]",it="[object String]",ot="[object WeakMap]",ut="[object ArrayBuffer]",at="[object Float32Array]",st="[object Float64Array]",ct="[object Int8Array]",ft="[object Int16Array]",lt="[object Int32Array]",ht="[object Uint8Array]",pt="[object Uint8ClampedArray]",_t="[object Uint16Array]",vt="[object Uint32Array]",dt=/\b__p \+= '';/g,yt=/\b(__p \+=) '' \+/g,gt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,mt=/&(?:amp|lt|gt|quot|#39|#96);/g,wt=/[&<>"'`]/g,Tt=RegExp(mt.source),bt=RegExp(wt.source),Et=/<%-([\s\S]+?)%>/g,Ot=/<%([\s\S]+?)%>/g,It=/<%=([\s\S]+?)%>/g,St=/\.|\[(?:[^[\]]+|(["'])(?:(?!\1)[^\n\\]|\\.)*?)\1\]/,At=/^\w*$/,Nt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,kt=/[.*+?^${}()|[\]\/\\]/g,Ct=RegExp(kt.source),Dt=/[\u0300-\u036f\ufe20-\ufe23]/g,xt=/\\(\\)?/g,Mt=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Rt=/\w*$/,jt=/^0[xX]/,Lt=/^\[object .+?Constructor\]$/,zt=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Pt=/($^)/,qt=/['\n\r\u2028\u2029\\]/g,Ut=function(){var t="[A-Z\\xc0-\\xd6\\xd8-\\xde]",e="[a-z\\xdf-\\xf6\\xf8-\\xff]+";return RegExp(t+"+(?="+t+e+")|"+t+"?"+e+"|"+t+"+|[0-9]+","g")}(),Wt=" \f \ufeff\n\r\u2028\u2029 ᠎              ",Vt=["Array","ArrayBuffer","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Math","Number","Object","RegExp","Set","String","_","clearTimeout","document","isFinite","parseInt","setTimeout","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","window"],Gt=-1,Kt={};Kt[at]=Kt[st]=Kt[ct]=Kt[ft]=Kt[lt]=Kt[ht]=Kt[pt]=Kt[_t]=Kt[vt]=!0,Kt[F]=Kt[B]=Kt[ut]=Kt[J]=Kt[Y]=Kt[X]=Kt[Z]=Kt[Q]=Kt[tt]=Kt[et]=Kt[nt]=Kt[rt]=Kt[it]=Kt[ot]=!1;var $t={};$t[F]=$t[B]=$t[ut]=$t[J]=$t[Y]=$t[at]=$t[st]=$t[ct]=$t[ft]=$t[lt]=$t[tt]=$t[et]=$t[nt]=$t[it]=$t[ht]=$t[pt]=$t[_t]=$t[vt]=!0,$t[X]=$t[Z]=$t[Q]=$t[rt]=$t[ot]=!1;var Ht={leading:!1,maxWait:0,trailing:!1},Ft={"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss"},Bt={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Jt={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},Yt={"function":!0,object:!0},Xt={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Zt=Yt[typeof e]&&e&&!e.nodeType&&e,Qt=Yt[typeof t]&&t&&!t.nodeType&&t,te=Zt&&Qt&&"object"==typeof i&&i&&i.Object&&i,ee=Yt[typeof self]&&self&&self.Object&&self,ne=Yt[typeof window]&&window&&window.Object&&window,re=(Qt&&Qt.exports===Zt&&Zt,te||ne!==(this&&this.window)&&ne||ee||this),ie=S();re._=ie,r=function(){return ie}.call(e,n,e,t),!(r!==A&&(t.exports=r))}).call(this)}).call(e,n(38)(t),function(){return this}())},function(t,e,n){"use strict";var r=function(t){return t&&t.__esModule?t:{"default":t}},i=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},o=function(){function t(t,e){for(var n=0;n0&&h["default"].dispatch({actionType:_["default"].ACTION_NEW_SERVICES,services:t})}function i(t){return u("homeassistant","turn_on",{entity_id:t})}function o(t){return u("homeassistant","turn_off",{entity_id:t})}function u(t,e){var n=void 0===arguments[2]?{}:arguments[2];return f["default"]("POST","services/"+t+"/"+e,n).then(function(r){v.notify("turn_on"==e&&n.entity_id?"Turned on "+n.entity_id+".":"turn_off"==e&&n.entity_id?"Turned off "+n.entity_id+".":"Service "+t+"/"+e+" called."),d.newStates(r)})}function a(){return f["default"]("GET","services").then(r)}var s=function(t){return t&&t.__esModule?t:{"default":t}};Object.defineProperty(e,"__esModule",{value:!0}),e.newServices=r,e.callTurnOn=i,e.callTurnOff=o,e.callService=u,e.fetchAll=a;var c=n(5),f=s(c),l=n(1),h=s(l),p=n(2),_=s(p),v=n(10),d=n(12)},function(t,e,n){"use strict";function r(t,e){(t.length>0||e)&&l["default"].dispatch({actionType:h.ACTION_NEW_STATES,states:t,replace:!!e})}function i(t,e){var n=void 0===arguments[2]?!1:arguments[2],i={state:e};n&&(i.attributes=n),c["default"]("POST","states/"+t,i).then(function(n){p.notify("State of "+t+" set to "+e+"."),r([n])})}function o(t){c["default"]("GET","states/"+t).then(function(t){r([t])})}function u(){c["default"]("GET","states").then(function(t){r(t,!0)})}var a=function(t){return t&&t.__esModule?t:{"default":t}};Object.defineProperty(e,"__esModule",{value:!0}),e.newStates=r,e.set=i,e.fetch=o,e.fetchAll=u;var s=n(5),c=a(s),f=n(1),l=a(f),h=n(2),p=n(10)},function(t,e,n){"use strict";function r(){f["default"].dispatch({actionType:h["default"].ACTION_FETCH_ALL}),_.fetchAll(),d.fetchAll(),g.fetchAll(),w.fetchAll(),b&&E()}function i(){b=!0,r()}function o(){b=!1,E.cancel()}var u=function(t){return t&&t.__esModule?t:{"default":t}};Object.defineProperty(e,"__esModule",{value:!0}),e.fetchAll=r,e.start=i,e.stop=o;var a=n(6),s=u(a),c=n(1),f=u(c),l=n(2),h=u(l),p=n(26),_=u(p),v=n(12),d=u(v),y=n(11),g=u(y),m=n(25),w=u(m),T=3e4,b=!1,E=s["default"].debounce(r,T)},function(t,e,n){"use strict";var r=function(t){return t&&t.__esModule?t:{"default":t}},i=function(t,e){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t)){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(s){i=!0,o=s}finally{try{!r&&a["return"]&&a["return"]()}finally{if(i)throw o}}return n}throw new TypeError("Invalid attempt to destructure non-iterable instance")},o=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},u=function(){function t(t,e){for(var n=0;nd}},{key:"all",get:function(){return g}}]),e}(p["default"]),w=new m;w.dispatchToken=c["default"].register(function(t){switch(t.actionType){case l["default"].ACTION_NEW_LOGBOOK:g=new a.List(t.logbookEntries.map(function(t){return v["default"].fromJSON(t)})),y=new Date,w.emitChange();break;case l["default"].ACTION_LOG_OUT:y=null,g=new a.List,w.emitChange()}}),e["default"]=w,t.exports=e["default"]},function(t,e,n){"use strict";function r(){return y.size}var i=function(t){return t&&t.__esModule?t:{"default":t}},o=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},u=function(){function t(t,e){for(var n=0;nv}},{key:"get",value:function(t){return g[t]||null}},{key:"all",get:function(){return s["default"].sortBy(s["default"].values(g),function(t){return t[0].entityId})}}]),e}(_["default"]),w=new m;w.dispatchToken=f["default"].register(function(t){switch(t.actionType){case h["default"].ACTION_NEW_STATE_HISTORY:s["default"].forEach(t.stateHistory,function(t){if(0!==t.length){var e=t[0].entityId;g[e]=t,y[e]=new Date}}),t.isFetchAll&&(d=new Date),w.emitChange();break;case h["default"].ACTION_LOG_OUT:d=null,y={},g={},w.emitChange()}}),e["default"]=w,t.exports=e["default"]},function(t,e,n){"use strict";function r(t){return-1!==y.indexOf(t)}function i(){return y.length===v}var o=function(t){return t&&t.__esModule?t:{"default":t}},u=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},a=function(){function t(t,e){for(var n=0;n0)&&f["default"].dispatch({actionType:h["default"].ACTION_NEW_STATE_HISTORY,stateHistory:e.map(function(t){return t.map(_["default"].fromJSON)}),isFetchAll:t})}function i(){s["default"]("GET","history/period").then(function(t){return r(!0,t)})}function o(t){s["default"]("GET","history/period?filter_entity_id="+t).then(function(t){return r(!1,t)})}var u=function(t){return t&&t.__esModule?t:{"default":t}};Object.defineProperty(e,"__esModule",{value:!0}),e.fetchAll=i,e.fetch=o;var a=n(5),s=u(a),c=n(1),f=u(c),l=n(2),h=u(l),p=n(14),_=u(p)},function(t,e,n){"use strict";function r(){return"webkitSpeechRecognition"in window}function i(){var t=g||y;l["default"].dispatch({actionType:p["default"].ACTION_LISTENING_TRANSMITTING,finalTranscript:t}),_.callService("conversation","process",{text:t}).then(function(){l["default"].dispatch({actionType:p["default"].ACTION_LISTENING_DONE,finalTranscript:t})},function(){l["default"].dispatch({actionType:p["default"].ACTION_LISTENING_ERROR})})}function o(){null!==d&&(d.onstart=null,d.onresult=null,d.onerror=null,d.onend=null,d.stop(),d=null,i()),y="",g=""}function u(){o(),window.r=d=new webkitSpeechRecognition,d.interimResults=!0,d.onstart=function(){l["default"].dispatch({actionType:p["default"].ACTION_LISTENING_START})},d.onresult=function(t){y="";for(var e=t.resultIndex;et||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},r.prototype.emit=function(t){var e,n,r,o,s,c;if(this._events||(this._events={}),"error"===t&&(!this._events.error||u(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(n=this._events[t],a(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:for(r=arguments.length,o=new Array(r-1),s=1;r>s;s++)o[s-1]=arguments[s];n.apply(this,o)}else if(u(n)){for(r=arguments.length,o=new Array(r-1),s=1;r>s;s++)o[s-1]=arguments[s];for(c=n.slice(),r=c.length,s=0;r>s;s++)c[s].apply(this,o)}return!0},r.prototype.addListener=function(t,e){var n;if(!i(e))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,i(e.listener)?e.listener:e),this._events[t]?u(this._events[t])?this._events[t].push(e):this._events[t]=[this._events[t],e]:this._events[t]=e,u(this._events[t])&&!this._events[t].warned){var n;n=a(this._maxListeners)?r.defaultMaxListeners:this._maxListeners,n&&n>0&&this._events[t].length>n&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},r.prototype.on=r.prototype.addListener,r.prototype.once=function(t,e){function n(){this.removeListener(t,n),r||(r=!0,e.apply(this,arguments))}if(!i(e))throw TypeError("listener must be a function");var r=!1;return n.listener=e,this.on(t,n),this},r.prototype.removeListener=function(t,e){var n,r,o,a;if(!i(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(n=this._events[t],o=n.length,r=-1,n===e||i(n.listener)&&n.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(u(n)){for(a=o;a-->0;)if(n[a]===e||n[a].listener&&n[a].listener===e){r=a;break}if(0>r)return this;1===n.length?(n.length=0,delete this._events[t]):n.splice(r,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},r.prototype.removeAllListeners=function(t){var e,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[t],i(n))this.removeListener(t,n);else for(;n.length;)this.removeListener(t,n[n.length-1]);return delete this._events[t],this},r.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?i(this._events[t])?[this._events[t]]:this._events[t].slice():[]},r.listenerCount=function(t,e){var n;return n=t._events&&t._events[e]?i(t._events[e])?1:t._events[e].length:0}},function(t,e,n){function r(t){return n(i(t))}function i(t){return o[t]||function(){throw new Error("Cannot find module '"+t+"'.")}()}var o={"./auth":7,"./auth.js":7,"./component":15,"./component.js":15,"./event":16,"./event.js":16,"./logbook":17,"./logbook.js":17,"./notification":18,"./notification.js":18,"./preference":19,"./preference.js":19,"./service":8,"./service.js":8,"./state":20,"./state.js":20,"./state_history":21,"./state_history.js":21,"./store":3,"./store.js":3,"./stream":9,"./stream.js":9,"./sync":22,"./sync.js":22,"./voice":23,"./voice.js":23};r.keys=function(){return Object.keys(o)},r.resolve=i,t.exports=r,r.id=40}]); \ No newline at end of file +return pickBy("isBefore",args)};moment.max=function(){var args=[].slice.call(arguments,0);return pickBy("isAfter",args)};moment.utc=function(input,format,locale,strict){var c;if(typeof locale==="boolean"){strict=locale;locale=undefined}c={};c._isAMomentObject=true;c._useUTC=true;c._isUTC=true;c._l=locale;c._i=input;c._f=format;c._strict=strict;c._pf=defaultParsingFlags();return makeMoment(c).utc()};moment.unix=function(input){return moment(input*1e3)};moment.duration=function(input,key){var duration=input,match=null,sign,ret,parseIso,diffRes;if(moment.isDuration(input)){duration={ms:input._milliseconds,d:input._days,M:input._months}}else if(typeof input==="number"){duration={};if(key){duration[key]=input}else{duration.milliseconds=input}}else if(!!(match=aspNetTimeSpanJsonRegex.exec(input))){sign=match[1]==="-"?-1:1;duration={y:0,d:toInt(match[DATE])*sign,h:toInt(match[HOUR])*sign,m:toInt(match[MINUTE])*sign,s:toInt(match[SECOND])*sign,ms:toInt(match[MILLISECOND])*sign}}else if(!!(match=isoDurationRegex.exec(input))){sign=match[1]==="-"?-1:1;parseIso=function(inp){var res=inp&&parseFloat(inp.replace(",","."));return(isNaN(res)?0:res)*sign};duration={y:parseIso(match[2]),M:parseIso(match[3]),d:parseIso(match[4]),h:parseIso(match[5]),m:parseIso(match[6]),s:parseIso(match[7]),w:parseIso(match[8])}}else if(duration==null){duration={}}else if(typeof duration==="object"&&("from"in duration||"to"in duration)){diffRes=momentsDifference(moment(duration.from),moment(duration.to));duration={};duration.ms=diffRes.milliseconds;duration.M=diffRes.months}ret=new Duration(duration);if(moment.isDuration(input)&&hasOwnProp(input,"_locale")){ret._locale=input._locale}return ret};moment.version=VERSION;moment.defaultFormat=isoFormat;moment.ISO_8601=function(){};moment.momentProperties=momentProperties;moment.updateOffset=function(){};moment.relativeTimeThreshold=function(threshold,limit){if(relativeTimeThresholds[threshold]===undefined){return false}if(limit===undefined){return relativeTimeThresholds[threshold]}relativeTimeThresholds[threshold]=limit;return true};moment.lang=deprecate("moment.lang is deprecated. Use moment.locale instead.",function(key,value){return moment.locale(key,value)});moment.locale=function(key,values){var data;if(key){if(typeof values!=="undefined"){data=moment.defineLocale(key,values)}else{data=moment.localeData(key)}if(data){moment.duration._locale=moment._locale=data}}return moment._locale._abbr};moment.defineLocale=function(name,values){if(values!==null){values.abbr=name;if(!locales[name]){locales[name]=new Locale}locales[name].set(values);moment.locale(name);return locales[name]}else{delete locales[name];return null}};moment.langData=deprecate("moment.langData is deprecated. Use moment.localeData instead.",function(key){return moment.localeData(key)});moment.localeData=function(key){var locale;if(key&&key._locale&&key._locale._abbr){key=key._locale._abbr}if(!key){return moment._locale}if(!isArray(key)){locale=loadLocale(key);if(locale){return locale}key=[key]}return chooseLocale(key)};moment.isMoment=function(obj){return obj instanceof Moment||obj!=null&&hasOwnProp(obj,"_isAMomentObject")};moment.isDuration=function(obj){return obj instanceof Duration};for(i=lists.length-1;i>=0;--i){makeList(lists[i])}moment.normalizeUnits=function(units){return normalizeUnits(units)};moment.invalid=function(flags){var m=moment.utc(NaN);if(flags!=null){extend(m._pf,flags)}else{m._pf.userInvalidated=true}return m};moment.parseZone=function(){return moment.apply(null,arguments).parseZone()};moment.parseTwoDigitYear=function(input){return toInt(input)+(toInt(input)>68?1900:2e3)};moment.isDate=isDate;extend(moment.fn=Moment.prototype,{clone:function(){return moment(this)},valueOf:function(){return+this._d-(this._offset||0)*6e4},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var m=moment(this).utc();if(00}return false},parsingFlags:function(){return extend({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(keepLocalTime){return this.utcOffset(0,keepLocalTime)},local:function(keepLocalTime){if(this._isUTC){this.utcOffset(0,keepLocalTime);this._isUTC=false;if(keepLocalTime){this.subtract(this._dateUtcOffset(),"m")}}return this},format:function(inputString){var output=formatMoment(this,inputString||moment.defaultFormat);return this.localeData().postformat(output)},add:createAdder(1,"add"),subtract:createAdder(-1,"subtract"),diff:function(input,units,asFloat){var that=makeAs(input,this),zoneDiff=(that.utcOffset()-this.utcOffset())*6e4,anchor,diff,output,daysAdjust;units=normalizeUnits(units);if(units==="year"||units==="month"||units==="quarter"){output=monthDiff(this,that);if(units==="quarter"){output=output/3}else if(units==="year"){output=output/12}}else{diff=this-that;output=units==="second"?diff/1e3:units==="minute"?diff/6e4:units==="hour"?diff/36e5:units==="day"?(diff-zoneDiff)/864e5:units==="week"?(diff-zoneDiff)/6048e5:diff}return asFloat?output:absRound(output)},from:function(time,withoutSuffix){return moment.duration({to:this,from:time}).locale(this.locale()).humanize(!withoutSuffix)},fromNow:function(withoutSuffix){return this.from(moment(),withoutSuffix)},calendar:function(time){var now=time||moment(),sod=makeAs(now,this).startOf("day"),diff=this.diff(sod,"days",true),format=diff<-6?"sameElse":diff<-1?"lastWeek":diff<0?"lastDay":diff<1?"sameDay":diff<2?"nextDay":diff<7?"nextWeek":"sameElse";return this.format(this.localeData().calendar(format,this,moment(now)))},isLeapYear:function(){return isLeapYear(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(input){var day=this._isUTC?this._d.getUTCDay():this._d.getDay();if(input!=null){input=parseWeekday(input,this.localeData());return this.add(input-day,"d")}else{return day}},month:makeAccessor("Month",true),startOf:function(units){units=normalizeUnits(units);switch(units){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}if(units==="week"){this.weekday(0)}else if(units==="isoWeek"){this.isoWeekday(1)}if(units==="quarter"){this.month(Math.floor(this.month()/3)*3)}return this},endOf:function(units){units=normalizeUnits(units);if(units===undefined||units==="millisecond"){return this}return this.startOf(units).add(1,units==="isoWeek"?"week":units).subtract(1,"ms")},isAfter:function(input,units){var inputMs;units=normalizeUnits(typeof units!=="undefined"?units:"millisecond");if(units==="millisecond"){input=moment.isMoment(input)?input:moment(input);return+this>+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return inputMs<+this.clone().startOf(units)}},isBefore:function(input,units){var inputMs;units=normalizeUnits(typeof units!=="undefined"?units:"millisecond");if(units==="millisecond"){input=moment.isMoment(input)?input:moment(input);return+this<+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return+this.clone().endOf(units)this?this:other}),zone:deprecate("moment().zone is deprecated, use moment().utcOffset instead. "+"https://github.com/moment/moment/issues/1779",function(input,keepLocalTime){if(input!=null){if(typeof input!=="string"){input=-input}this.utcOffset(input,keepLocalTime);return this}else{return-this.utcOffset()}}),utcOffset:function(input,keepLocalTime){var offset=this._offset||0,localAdjust;if(input!=null){if(typeof input==="string"){input=utcOffsetFromString(input)}if(Math.abs(input)<16){input=input*60}if(!this._isUTC&&keepLocalTime){localAdjust=this._dateUtcOffset()}this._offset=input;this._isUTC=true;if(localAdjust!=null){this.add(localAdjust,"m")}if(offset!==input){if(!keepLocalTime||this._changeInProgress){addOrSubtractDurationFromMoment(this,moment.duration(input-offset,"m"),1,false)}else if(!this._changeInProgress){this._changeInProgress=true;moment.updateOffset(this,true);this._changeInProgress=null}}return this}else{return this._isUTC?offset:this._dateUtcOffset()}},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&this._offset===0},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){if(this._tzm){this.utcOffset(this._tzm)}else if(typeof this._i==="string"){this.utcOffset(utcOffsetFromString(this._i))}return this},hasAlignedHourOffset:function(input){if(!input){input=0}else{input=moment(input).utcOffset()}return(this.utcOffset()-input)%60===0},daysInMonth:function(){return daysInMonth(this.year(),this.month())},dayOfYear:function(input){var dayOfYear=round((moment(this).startOf("day")-moment(this).startOf("year"))/864e5)+1;return input==null?dayOfYear:this.add(input-dayOfYear,"d")},quarter:function(input){return input==null?Math.ceil((this.month()+1)/3):this.month((input-1)*3+this.month()%3)},weekYear:function(input){var year=weekOfYear(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return input==null?year:this.add(input-year,"y")},isoWeekYear:function(input){var year=weekOfYear(this,1,4).year;return input==null?year:this.add(input-year,"y")},week:function(input){var week=this.localeData().week(this);return input==null?week:this.add((input-week)*7,"d")},isoWeek:function(input){var week=weekOfYear(this,1,4).week;return input==null?week:this.add((input-week)*7,"d")},weekday:function(input){var weekday=(this.day()+7-this.localeData()._week.dow)%7;return input==null?weekday:this.add(input-weekday,"d")},isoWeekday:function(input){return input==null?this.day()||7:this.day(this.day()%7?input:input-7)},isoWeeksInYear:function(){return weeksInYear(this.year(),1,4)},weeksInYear:function(){var weekInfo=this.localeData()._week;return weeksInYear(this.year(),weekInfo.dow,weekInfo.doy)},get:function(units){units=normalizeUnits(units);return this[units]()},set:function(units,value){var unit;if(typeof units==="object"){for(unit in units){this.set(unit,units[unit])}}else{units=normalizeUnits(units);if(typeof this[units]==="function"){this[units](value)}}return this},locale:function(key){var newLocaleData;if(key===undefined){return this._locale._abbr}else{newLocaleData=moment.localeData(key);if(newLocaleData!=null){this._locale=newLocaleData}return this}},lang:deprecate("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(key){if(key===undefined){return this.localeData()}else{return this.locale(key)}}),localeData:function(){return this._locale},_dateUtcOffset:function(){return-Math.round(this._d.getTimezoneOffset()/15)*15}});function rawMonthSetter(mom,value){var dayOfMonth;if(typeof value==="string"){value=mom.localeData().monthsParse(value);if(typeof value!=="number"){return mom}}dayOfMonth=Math.min(mom.date(),daysInMonth(mom.year(),value));mom._d["set"+(mom._isUTC?"UTC":"")+"Month"](value,dayOfMonth);return mom}function rawGetter(mom,unit){return mom._d["get"+(mom._isUTC?"UTC":"")+unit]()}function rawSetter(mom,unit,value){if(unit==="Month"){return rawMonthSetter(mom,value)}else{return mom._d["set"+(mom._isUTC?"UTC":"")+unit](value)}}function makeAccessor(unit,keepTime){return function(value){if(value!=null){rawSetter(this,unit,value);moment.updateOffset(this,keepTime);return this}else{return rawGetter(this,unit)}}}moment.fn.millisecond=moment.fn.milliseconds=makeAccessor("Milliseconds",false);moment.fn.second=moment.fn.seconds=makeAccessor("Seconds",false);moment.fn.minute=moment.fn.minutes=makeAccessor("Minutes",false);moment.fn.hour=moment.fn.hours=makeAccessor("Hours",true);moment.fn.date=makeAccessor("Date",true);moment.fn.dates=deprecate("dates accessor is deprecated. Use date instead.",makeAccessor("Date",true));moment.fn.year=makeAccessor("FullYear",true);moment.fn.years=deprecate("years accessor is deprecated. Use year instead.",makeAccessor("FullYear",true));moment.fn.days=moment.fn.day;moment.fn.months=moment.fn.month;moment.fn.weeks=moment.fn.week;moment.fn.isoWeeks=moment.fn.isoWeek;moment.fn.quarters=moment.fn.quarter;moment.fn.toJSON=moment.fn.toISOString;moment.fn.isUTC=moment.fn.isUtc;function daysToYears(days){return days*400/146097}function yearsToDays(years){return years*146097/400}extend(moment.duration.fn=Duration.prototype,{_bubble:function(){var milliseconds=this._milliseconds,days=this._days,months=this._months,data=this._data,seconds,minutes,hours,years=0;data.milliseconds=milliseconds%1e3;seconds=absRound(milliseconds/1e3);data.seconds=seconds%60;minutes=absRound(seconds/60);data.minutes=minutes%60;hours=absRound(minutes/60);data.hours=hours%24;days+=absRound(hours/24);years=absRound(daysToYears(days));days-=absRound(yearsToDays(years));months+=absRound(days/30);days%=30;years+=absRound(months/12);months%=12;data.days=days;data.months=months;data.years=years},abs:function(){this._milliseconds=Math.abs(this._milliseconds);this._days=Math.abs(this._days);this._months=Math.abs(this._months);this._data.milliseconds=Math.abs(this._data.milliseconds);this._data.seconds=Math.abs(this._data.seconds);this._data.minutes=Math.abs(this._data.minutes);this._data.hours=Math.abs(this._data.hours);this._data.months=Math.abs(this._data.months);this._data.years=Math.abs(this._data.years);return this},weeks:function(){return absRound(this.days()/7)},valueOf:function(){return this._milliseconds+this._days*864e5+this._months%12*2592e6+toInt(this._months/12)*31536e6},humanize:function(withSuffix){var output=relativeTime(this,!withSuffix,this.localeData());if(withSuffix){output=this.localeData().pastFuture(+this,output)}return this.localeData().postformat(output)},add:function(input,val){var dur=moment.duration(input,val);this._milliseconds+=dur._milliseconds;this._days+=dur._days;this._months+=dur._months;this._bubble();return this},subtract:function(input,val){var dur=moment.duration(input,val);this._milliseconds-=dur._milliseconds;this._days-=dur._days;this._months-=dur._months;this._bubble();return this},get:function(units){units=normalizeUnits(units);return this[units.toLowerCase()+"s"]()},as:function(units){var days,months;units=normalizeUnits(units);if(units==="month"||units==="year"){days=this._days+this._milliseconds/864e5;months=this._months+daysToYears(days)*12;return units==="month"?months:months/12}else{days=this._days+Math.round(yearsToDays(this._months/12));switch(units){case"week":return days/7+this._milliseconds/6048e5;case"day":return days+this._milliseconds/864e5;case"hour":return days*24+this._milliseconds/36e5;case"minute":return days*24*60+this._milliseconds/6e4;case"second":return days*24*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(days*24*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+units)}}},lang:moment.fn.lang,locale:moment.fn.locale,toIsoString:deprecate("toIsoString() is deprecated. Please use toISOString() instead "+"(notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var years=Math.abs(this.years()),months=Math.abs(this.months()),days=Math.abs(this.days()),hours=Math.abs(this.hours()),minutes=Math.abs(this.minutes()),seconds=Math.abs(this.seconds()+this.milliseconds()/1e3);if(!this.asSeconds()){return"P0D"}return(this.asSeconds()<0?"-":"")+"P"+(years?years+"Y":"")+(months?months+"M":"")+(days?days+"D":"")+(hours||minutes||seconds?"T":"")+(hours?hours+"H":"")+(minutes?minutes+"M":"")+(seconds?seconds+"S":"")},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}});moment.duration.fn.toString=moment.duration.fn.toISOString;function makeDurationGetter(name){moment.duration.fn[name]=function(){return this._data[name]}}for(i in unitMillisecondFactors){if(hasOwnProp(unitMillisecondFactors,i)){makeDurationGetter(i.toLowerCase())}}moment.duration.fn.asMilliseconds=function(){return this.as("ms")};moment.duration.fn.asSeconds=function(){return this.as("s")};moment.duration.fn.asMinutes=function(){return this.as("m")};moment.duration.fn.asHours=function(){return this.as("h")};moment.duration.fn.asDays=function(){return this.as("d")};moment.duration.fn.asWeeks=function(){return this.as("weeks")};moment.duration.fn.asMonths=function(){return this.as("M")};moment.duration.fn.asYears=function(){return this.as("y")};moment.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(number){var b=number%10,output=toInt(number%100/10)===1?"th":b===1?"st":b===2?"nd":b===3?"rd":"th";return number+output}});function makeGlobal(shouldDeprecate){if(typeof ender!=="undefined"){return}oldGlobalMoment=globalScope.moment;if(shouldDeprecate){globalScope.moment=deprecate("Accessing Moment through the global scope is "+"deprecated, and will be removed in an upcoming "+"release.",moment)}else{globalScope.moment=moment}}if(hasModule){module.exports=moment}else if(typeof define==="function"&&define.amd){define(function(require,exports,module){if(module.config&&module.config()&&module.config().noGlobal===true){globalScope.moment=oldGlobalMoment}return moment});makeGlobal(true)}else{makeGlobal()}}).call(this); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-sun.html b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-sun.html index 96c92357d1b..8a2969db371 100644 --- a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-sun.html +++ b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-sun.html @@ -11,38 +11,43 @@
- Rising + Rising
- {{stateObj.attributes.next_rising | HATimeStripDate}} + {{rising | formatTime}}
- Setting + Setting
- {{stateObj.attributes.next_setting | HATimeStripDate}} + {{setting | formatTime}}
From 040dd3c40999c517d1cd8963e709aa36315a4b14 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Apr 2015 08:18:53 -0700 Subject: [PATCH 110/842] Strip microseconds on state.last_updated --- homeassistant/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 7d6a1f03d00..301a403193d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -496,7 +496,8 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = last_updated or date_util.utcnow() + self.last_updated = date_util.strip_microseconds( + last_updated or date_util.utcnow()) # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) From 5105c5f200d343383583f383566535c911436416 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Apr 2015 09:01:27 -0700 Subject: [PATCH 111/842] Update coveragerc to hide latest device components --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 4b317feb12c..2e9ee27dcb5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = homeassistant/components/zwave.py homeassistant/components/*/zwave.py + homeassistant/components/modbus.py + homeassistant/components/*/modbus.py + homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py @@ -23,6 +26,8 @@ omit = homeassistant/components/sensor/mysensors.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushover.py + homeassistant/components/notify/instapush.py + homeassistant/components/notify/nma.py homeassistant/components/media_player/cast.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/tomato.py From ef470de3ca5c9f703079739a60b081516e9b50bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Apr 2015 09:01:27 -0700 Subject: [PATCH 112/842] Update coveragerc to hide latest device components --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 4b317feb12c..2e9ee27dcb5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = homeassistant/components/zwave.py homeassistant/components/*/zwave.py + homeassistant/components/modbus.py + homeassistant/components/*/modbus.py + homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py @@ -23,6 +26,8 @@ omit = homeassistant/components/sensor/mysensors.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushover.py + homeassistant/components/notify/instapush.py + homeassistant/components/notify/nma.py homeassistant/components/media_player/cast.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/tomato.py From fd67e47128ff4237027ccb239ea92e9666c0b594 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Apr 2015 22:26:54 -0700 Subject: [PATCH 113/842] Add tests for device_sun_light_trigger --- .../custom_components/device_tracker/test.py | 4 + tests/helpers.py | 34 ++++- ...test_component_device_sun_light_trigger.py | 127 ++++++++++++++++++ tests/test_component_device_tracker.py | 2 + 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 tests/test_component_device_sun_light_trigger.py diff --git a/tests/config/custom_components/device_tracker/test.py b/tests/config/custom_components/device_tracker/test.py index 481892a9a67..635d400316f 100644 --- a/tests/config/custom_components/device_tracker/test.py +++ b/tests/config/custom_components/device_tracker/test.py @@ -26,6 +26,10 @@ class MockScanner(object): """ Make a device leave the house. """ self.devices_home.remove(device) + def reset(self): + """ Resets which devices are home. """ + self.devices_home = [] + def scan_devices(self): """ Returns a list of fake devices. """ diff --git a/tests/helpers.py b/tests/helpers.py index 33b4468cfac..f9ccfac94c9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,10 +5,14 @@ tests.helper Helper method for writing tests. """ import os +from datetime import timedelta import homeassistant as ha +import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity -from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME +from homeassistant.const import ( + STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED) +from homeassistant.components import sun def get_test_config_dir(): @@ -20,6 +24,8 @@ def get_test_home_assistant(): """ Returns a Home Assistant object pointing at test config dir. """ hass = ha.HomeAssistant() hass.config.config_dir = get_test_config_dir() + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 return hass @@ -37,6 +43,32 @@ def mock_service(hass, domain, service): return calls +def trigger_device_tracker_scan(hass): + """ Triggers the device tracker to scan. """ + hass.bus.fire( + EVENT_TIME_CHANGED, + {'now': + dt_util.utcnow().replace(second=0) + timedelta(hours=1)}) + + +def ensure_sun_risen(hass): + """ Trigger sun to rise if below horizon. """ + if not sun.is_on(hass): + hass.bus.fire( + EVENT_TIME_CHANGED, + {'now': + sun.next_rising_utc(hass) + timedelta(seconds=10)}) + + +def ensure_sun_set(hass): + """ Trigger sun to set if above horizon. """ + if sun.is_on(hass): + hass.bus.fire( + EVENT_TIME_CHANGED, + {'now': + sun.next_setting_utc(hass) + timedelta(seconds=10)}) + + class MockModule(object): """ Provides a fake module. """ diff --git a/tests/test_component_device_sun_light_trigger.py b/tests/test_component_device_sun_light_trigger.py new file mode 100644 index 00000000000..7a05f63099f --- /dev/null +++ b/tests/test_component_device_sun_light_trigger.py @@ -0,0 +1,127 @@ +""" +tests.test_component_device_sun_light_trigger +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests device sun light trigger component. +""" +# pylint: disable=too-many-public-methods,protected-access +import os +import unittest + +import homeassistant.loader as loader +from homeassistant.const import CONF_PLATFORM +from homeassistant.components import ( + device_tracker, light, sun, device_sun_light_trigger) + + +from helpers import ( + get_test_home_assistant, ensure_sun_risen, ensure_sun_set, + trigger_device_tracker_scan) + + +KNOWN_DEV_PATH = None + + +def setUpModule(): # pylint: disable=invalid-name + """ Initalizes a Home Assistant server. """ + global KNOWN_DEV_PATH + + hass = get_test_home_assistant() + + loader.prepare(hass) + KNOWN_DEV_PATH = hass.config.path( + device_tracker.KNOWN_DEVICES_FILE) + + hass.stop() + + with open(KNOWN_DEV_PATH, 'w') as fil: + fil.write('device,name,track,picture\n') + fil.write('DEV1,device 1,1,http://example.com/dev1.jpg\n') + fil.write('DEV2,device 2,1,http://example.com/dev2.jpg\n') + + +def tearDownModule(): # pylint: disable=invalid-name + """ Stops the Home Assistant server. """ + os.remove(KNOWN_DEV_PATH) + + +class TestDeviceSunLightTrigger(unittest.TestCase): + """ Test the device sun light trigger module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + + self.scanner = loader.get_component( + 'device_tracker.test').get_scanner(None, None) + + self.scanner.reset() + self.scanner.come_home('DEV1') + + loader.get_component('light.test').init() + + device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} + }) + + light.setup(self.hass, { + light.DOMAIN: {CONF_PLATFORM: 'test'} + }) + + sun.setup(self.hass, {}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_lights_on_when_sun_sets(self): + """ Test lights go on when there is someone home and the sun sets. """ + + device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}}) + + ensure_sun_risen(self.hass) + + light.turn_off(self.hass) + + self.hass.pool.block_till_done() + + ensure_sun_set(self.hass) + + self.hass.pool.block_till_done() + + self.assertTrue(light.is_on(self.hass)) + + def test_lights_turn_off_when_everyone_leaves(self): + """ Test lights turn off when everyone leaves the house. """ + light.turn_on(self.hass) + + self.hass.pool.block_till_done() + + device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}}) + + self.scanner.leave_home('DEV1') + + trigger_device_tracker_scan(self.hass) + + self.hass.pool.block_till_done() + + self.assertFalse(light.is_on(self.hass)) + + def test_lights_turn_on_when_coming_home_after_sun_set(self): + """ Test lights turn on when coming home after sun set. """ + light.turn_off(self.hass) + + ensure_sun_set(self.hass) + + self.hass.pool.block_till_done() + + device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}}) + + self.scanner.come_home('DEV2') + trigger_device_tracker_scan(self.hass) + + self.hass.pool.block_till_done() + + self.assertTrue(light.is_on(self.hass)) diff --git a/tests/test_component_device_tracker.py b/tests/test_component_device_tracker.py index 74af55fdc66..038b2363e7b 100644 --- a/tests/test_component_device_tracker.py +++ b/tests/test_component_device_tracker.py @@ -81,6 +81,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): scanner = loader.get_component( 'device_tracker.test').get_scanner(None, None) + scanner.reset() + scanner.come_home('DEV1') scanner.come_home('DEV2') From cf5278b7e9f6f60f9abe36c438a73c6933387348 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Apr 2015 23:21:31 -0700 Subject: [PATCH 114/842] Add tests for recorder component --- homeassistant/__init__.py | 14 +++++- homeassistant/components/recorder.py | 7 +++ tests/test_component_recorder.py | 70 ++++++++++++++++++++++++++++ tests/test_core.py | 6 ++- 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/test_component_recorder.py diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 301a403193d..2beb749ec6b 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -376,6 +376,13 @@ class Event(object): return "".format(self.event_type, str(self.origin)[0]) + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self.event_type == other.event_type and + self.data == other.data and + self.origin == other.origin and + self.time_fired == other.time_fired) + class EventBus(object): """ Class that allows different components to communicate via services @@ -398,6 +405,9 @@ class EventBus(object): def fire(self, event_type, event_data=None, origin=EventOrigin.local): """ Fire an event. """ + if not self._pool.running: + raise HomeAssistantError('Home Assistant has shut down.') + with self._lock: # Copy the list of the current listeners because some listeners # remove themselves as a listener while being executed which @@ -898,7 +908,9 @@ class Timer(threading.Thread): last_fired_on_second = now.second - self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + # Event might have been set while sleeping + if not self._stop_event.isSet(): + self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) class Config(object): diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 4f0a76cf18a..717b6514bb4 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -189,9 +189,11 @@ class Recorder(threading.Thread): if event == self.quit_object: self._close_run() self._close_connection() + self.queue.task_done() return elif event.event_type == EVENT_TIME_CHANGED: + self.queue.task_done() continue elif event.event_type == EVENT_STATE_CHANGED: @@ -199,6 +201,7 @@ class Recorder(threading.Thread): event.data['entity_id'], event.data.get('new_state')) self.record_event(event) + self.queue.task_done() def event_listener(self, event): """ Listens for new events on the EventBus and puts them @@ -266,6 +269,10 @@ class Recorder(threading.Thread): "Error querying the database using: %s", sql_query) return [] + def block_till_done(self): + """ Blocks till all events processed. """ + self.queue.join() + def _setup_connection(self): """ Ensure database is ready to fly. """ db_path = self.hass.config.path(DB_FILE) diff --git a/tests/test_component_recorder.py b/tests/test_component_recorder.py new file mode 100644 index 00000000000..68c63b637d0 --- /dev/null +++ b/tests/test_component_recorder.py @@ -0,0 +1,70 @@ +""" +tests.test_component_recorder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests Recorder component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest +import os + +from homeassistant.const import MATCH_ALL +from homeassistant.components import recorder + +from helpers import get_test_home_assistant + + +class TestRecorder(unittest.TestCase): + """ Test the chromecast module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + recorder.setup(self.hass, {}) + self.hass.start() + recorder._INSTANCE.block_till_done() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + recorder._INSTANCE.block_till_done() + os.remove(self.hass.config.path(recorder.DB_FILE)) + + def test_saving_state(self): + """ Tests saving and restoring a state. """ + entity_id = 'test.recorder' + state = 'restoring_from_db' + attributes = {'test_attr': 5, 'test_attr_10': 'nice'} + + self.hass.states.set(entity_id, state, attributes) + + self.hass.pool.block_till_done() + recorder._INSTANCE.block_till_done() + + states = recorder.query_states('SELECT * FROM states') + + self.assertEqual(1, len(states)) + self.assertEqual(self.hass.states.get(entity_id), states[0]) + + def test_saving_event(self): + """ Tests saving and restoring an event. """ + event_type = 'EVENT_TEST' + event_data = {'test_attr': 5, 'test_attr_10': 'nice'} + + events = [] + + def event_listener(event): + """ Records events from eventbus. """ + if event.event_type == event_type: + events.append(event) + + self.hass.bus.listen(MATCH_ALL, event_listener) + + self.hass.bus.fire(event_type, event_data) + + self.hass.pool.block_till_done() + recorder._INSTANCE.block_till_done() + + db_events = recorder.query_events( + 'SELECT * FROM events WHERE event_type = ?', (event_type, )) + + self.assertEqual(events, db_events) diff --git a/tests/test_core.py b/tests/test_core.py index b450479f2d9..58052fe43f0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -30,7 +30,11 @@ class TestHomeAssistant(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.hass.stop() + try: + self.hass.stop() + except ha.HomeAssistantError: + # Already stopped after the block till stopped test + pass def test_get_config_path(self): """ Test get_config_path method. """ From 17cc9a43bb303284dd6c5711e4c80eac23cde8e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2015 21:03:01 -0700 Subject: [PATCH 115/842] Add tests for history component --- homeassistant/__init__.py | 6 +- homeassistant/components/history.py | 2 +- tests/helpers.py | 25 +++++- tests/test_component_history.py | 134 ++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 tests/test_component_history.py diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 2beb749ec6b..6a7b328ba40 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -910,7 +910,11 @@ class Timer(threading.Thread): # Event might have been set while sleeping if not self._stop_event.isSet(): - self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + try: + self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + except HomeAssistantError: + # HA raises error if firing event after it has shut down + break class Config(object): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 2717cdccd7f..14be60fa97e 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -23,7 +23,7 @@ def last_5_states(entity_id): query = """ SELECT * FROM states WHERE entity_id=? AND last_changed=last_updated - ORDER BY last_changed DESC LIMIT 0, 5 + ORDER BY state_id DESC LIMIT 0, 5 """ return recorder.query_states(query, (entity_id, )) diff --git a/tests/helpers.py b/tests/helpers.py index f9ccfac94c9..c6799defe21 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -11,7 +11,8 @@ import homeassistant as ha import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED) + STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, + EVENT_STATE_CHANGED) from homeassistant.components import sun @@ -20,9 +21,17 @@ def get_test_config_dir(): return os.path.join(os.path.dirname(__file__), "config") -def get_test_home_assistant(): +def get_test_home_assistant(num_threads=None): """ Returns a Home Assistant object pointing at test config dir. """ + if num_threads: + orig_num_threads = ha.MIN_WORKER_THREAD + ha.MIN_WORKER_THREAD = num_threads + hass = ha.HomeAssistant() + + if num_threads: + ha.MIN_WORKER_THREAD = orig_num_threads + hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 @@ -69,6 +78,18 @@ def ensure_sun_set(hass): sun.next_setting_utc(hass) + timedelta(seconds=10)}) +def mock_state_change_event(hass, new_state, old_state=None): + event_data = { + 'entity_id': new_state.entity_id, + 'new_state': new_state, + } + + if old_state: + event_data['old_state'] = old_state + + hass.bus.fire(EVENT_STATE_CHANGED, event_data) + + class MockModule(object): """ Provides a fake module. """ diff --git a/tests/test_component_history.py b/tests/test_component_history.py new file mode 100644 index 00000000000..86d91c14f22 --- /dev/null +++ b/tests/test_component_history.py @@ -0,0 +1,134 @@ +""" +tests.test_component_history +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the history component. +""" +# pylint: disable=protected-access,too-many-public-methods +import time +import os +import unittest + +import homeassistant as ha +import homeassistant.util.dt as dt_util +from homeassistant.components import history, recorder, http + +from helpers import get_test_home_assistant, mock_state_change_event + + +class TestComponentHistory(unittest.TestCase): + """ Tests homeassistant.components.history module. """ + + def setUp(self): # pylint: disable=invalid-name + """ Init needed objects. """ + self.hass = get_test_home_assistant(1) + self.init_rec = False + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + if self.init_rec: + recorder._INSTANCE.block_till_done() + os.remove(self.hass.config.path(recorder.DB_FILE)) + + def init_recorder(self): + recorder.setup(self.hass, {}) + self.hass.start() + recorder._INSTANCE.block_till_done() + self.init_rec = True + + def test_setup(self): + """ Test setup method of history. """ + http.setup(self.hass, {http.DOMAIN: {}}) + self.assertTrue(history.setup(self.hass, {})) + + def test_last_5_states(self): + """ Test retrieving the last 5 states. """ + self.init_recorder() + states = [] + + entity_id = 'test.last_5_states' + + for i in range(7): + self.hass.states.set(entity_id, "State {}".format(i)) + + if i > 1: + states.append(self.hass.states.get(entity_id)) + + self.hass.pool.block_till_done() + recorder._INSTANCE.block_till_done() + + self.assertEqual( + list(reversed(states)), history.last_5_states(entity_id)) + + def test_get_states(self): + """ Test getting states at a specific point in time. """ + self.init_recorder() + states = [] + + # Create 10 states for 5 different entities + # After the first 5, sleep a second and save the time + # history.get_states takes the latest states BEFORE point X + + for i in range(10): + state = ha.State( + 'test.point_in_time_{}'.format(i % 5), + "State {}".format(i), + {'attribute_test': i}) + + mock_state_change_event(self.hass, state) + self.hass.pool.block_till_done() + recorder._INSTANCE.block_till_done() + + if i < 5: + states.append(state) + + if i == 4: + time.sleep(1) + point = dt_util.utcnow() + + self.assertEqual( + states, + sorted( + history.get_states(point), key=lambda state: state.entity_id)) + + # Test get_state here because we have a DB setup + self.assertEqual( + states[0], history.get_state(point, states[0].entity_id)) + + def test_state_changes_during_period(self): + self.init_recorder() + entity_id = 'media_player.test' + + def set_state(state): + self.hass.states.set(entity_id, state) + self.hass.pool.block_till_done() + recorder._INSTANCE.block_till_done() + + return self.hass.states.get(entity_id) + + set_state('idle') + set_state('YouTube') + + start = dt_util.utcnow() + + time.sleep(1) + + states = [ + set_state('idle'), + set_state('Netflix'), + set_state('Plex'), + set_state('YouTube'), + ] + + time.sleep(1) + + end = dt_util.utcnow() + + set_state('Netflix') + set_state('Plex') + + self.assertEqual( + {entity_id: states}, + history.state_changes_during_period(start, end, entity_id)) From 2cad421de939d9089f68564e6c29b08817d58fcf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2015 21:47:20 -0700 Subject: [PATCH 116/842] Add tests for logbook component --- tests/test_component_logbook.py | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_component_logbook.py diff --git a/tests/test_component_logbook.py b/tests/test_component_logbook.py new file mode 100644 index 00000000000..99b38de1085 --- /dev/null +++ b/tests/test_component_logbook.py @@ -0,0 +1,95 @@ +""" +tests.test_component_logbook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the logbook component. +""" +# pylint: disable=protected-access,too-many-public-methods +import unittest +from datetime import timedelta + +import homeassistant as ha +from homeassistant.const import ( + EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.util.dt as dt_util +from homeassistant.components import logbook, http + +from helpers import get_test_home_assistant + + +class TestComponentHistory(unittest.TestCase): + """ Tests homeassistant.components.history module. """ + + def test_setup(self): + """ Test setup method. """ + try: + hass = get_test_home_assistant() + http.setup(hass, {}) + self.assertTrue(logbook.setup(hass, {})) + finally: + hass.stop() + + def test_humanify_filter_sensor(self): + """ Test humanify filter too frequent sensor values. """ + entity_id = 'sensor.bla' + + pointA = dt_util.strip_microseconds(dt_util.utcnow().replace(minute=2)) + pointB = pointA.replace(minute=5) + pointC = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) + + eventA = self.create_state_changed_event(pointA, entity_id, 10) + eventB = self.create_state_changed_event(pointB, entity_id, 20) + eventC = self.create_state_changed_event(pointC, entity_id, 30) + + entries = list(logbook.humanify((eventA, eventB, eventC))) + + self.assertEqual(2, len(entries)) + self.assert_entry( + entries[0], pointB, 'bla', domain='sensor', entity_id=entity_id) + + self.assert_entry( + entries[1], pointC, 'bla', domain='sensor', entity_id=entity_id) + + def test_home_assistant_start_stop_grouped(self): + """ Tests if home assistant start and stop events are grouped if + occuring in the same minute. """ + entries = list(logbook.humanify(( + ha.Event(EVENT_HOMEASSISTANT_STOP), + ha.Event(EVENT_HOMEASSISTANT_START), + ))) + + self.assertEqual(1, len(entries)) + self.assert_entry( + entries[0], name='Home Assistant', message='restarted', + domain=ha.DOMAIN) + + def assert_entry(self, entry, when=None, name=None, message=None, + domain=None, entity_id=None): + """ Asserts an entry is what is expected """ + if when: + self.assertEqual(when, entry.when) + + if name: + self.assertEqual(name, entry.name) + + if message: + self.assertEqual(message, entry.message) + + if domain: + self.assertEqual(domain, entry.domain) + + if entity_id: + self.assertEqual(entity_id, entry.entity_id) + + def create_state_changed_event(self, event_time_fired, entity_id, state): + """ Create state changed event. """ + + # Logbook only cares about state change events that + # contain an old state but will not actually act on it. + state = ha.State(entity_id, state).as_dict() + + return ha.Event(EVENT_STATE_CHANGED, { + 'entity_id': entity_id, + 'old_state': state, + 'new_state': state, + }, time_fired=event_time_fired) From 29e376a66e988d29fae7ac5fbbdfe0975fe1fcfd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2015 21:59:24 -0700 Subject: [PATCH 117/842] Assign unique ports for history and logbook tests --- tests/test_component_history.py | 5 ++++- tests/test_component_logbook.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_component_history.py b/tests/test_component_history.py index 86d91c14f22..b6ae8dab33f 100644 --- a/tests/test_component_history.py +++ b/tests/test_component_history.py @@ -15,6 +15,8 @@ from homeassistant.components import history, recorder, http from helpers import get_test_home_assistant, mock_state_change_event +SERVER_PORT = 8126 + class TestComponentHistory(unittest.TestCase): """ Tests homeassistant.components.history module. """ @@ -40,7 +42,8 @@ class TestComponentHistory(unittest.TestCase): def test_setup(self): """ Test setup method of history. """ - http.setup(self.hass, {http.DOMAIN: {}}) + http.setup(self.hass, { + http.DOMAIN: {http.CONF_SERVER_PORT: SERVER_PORT}}) self.assertTrue(history.setup(self.hass, {})) def test_last_5_states(self): diff --git a/tests/test_component_logbook.py b/tests/test_component_logbook.py index 99b38de1085..2f8f6b8c513 100644 --- a/tests/test_component_logbook.py +++ b/tests/test_component_logbook.py @@ -16,6 +16,8 @@ from homeassistant.components import logbook, http from helpers import get_test_home_assistant +SERVER_PORT = 8127 + class TestComponentHistory(unittest.TestCase): """ Tests homeassistant.components.history module. """ @@ -24,7 +26,8 @@ class TestComponentHistory(unittest.TestCase): """ Test setup method. """ try: hass = get_test_home_assistant() - http.setup(hass, {}) + http.setup(hass, { + http.DOMAIN: {http.CONF_SERVER_PORT: SERVER_PORT}}) self.assertTrue(logbook.setup(hass, {})) finally: hass.stop() From 09f1983d406586cecde586b506d9461a9b63df81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2015 22:44:24 -0700 Subject: [PATCH 118/842] Update dependency check in __main__ --- homeassistant/__main__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index a1f9b9e4818..83a8c499718 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -7,6 +7,14 @@ import argparse import importlib +# Home Assistant dependencies, mapped module -> package name +DEPENDENCIES = { + 'requests': 'requests', + 'yaml': 'pyyaml', + 'pytz': 'pytz', +} + + def validate_python(): """ Validate we're running the right Python version. """ major, minor = sys.version_info[:2] @@ -20,13 +28,13 @@ def validate_dependencies(): """ Validate all dependencies that HA uses. """ import_fail = False - for module in ['requests']: + for module, name in DEPENDENCIES.items(): try: importlib.import_module(module) except ImportError: import_fail = True print( - 'Fatal Error: Unable to find dependency {}'.format(module)) + 'Fatal Error: Unable to find dependency {}'.format(name)) if import_fail: print(("Install dependencies by running: " From 2196f77081d29ea51c0e4ab486f8c1edae8df382 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2015 22:44:41 -0700 Subject: [PATCH 119/842] Exclude __main__ from coveragerc --- .coveragerc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2e9ee27dcb5..2115ee07b3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,8 @@ source = homeassistant omit = + homeassistant/__main__.py + homeassistant/external/* # omit pieces of code that rely on external devices being present @@ -14,10 +16,14 @@ omit = homeassistant/components/modbus.py homeassistant/components/*/modbus.py + homeassistant/components/isy994.py + homeassistant/components/*/isy994.py + homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py homeassistant/components/keyboard.py + homeassistant/components/browser.py homeassistant/components/switch/wemo.py homeassistant/components/thermostat/nest.py homeassistant/components/light/hue.py @@ -36,11 +42,6 @@ omit = homeassistant/components/device_tracker/ddwrt.py homeassistant/components/sensor/transmission.py - homeassistant/components/isy994.py - homeassistant/components/light/isy994.py - homeassistant/components/switch/isy994.py - homeassistant/components/sensor/isy994.py - [report] # Regexes for lines to exclude from consideration From 20bdc82cba84af5e7ec44f943e9999e10830a110 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2015 06:14:38 -0700 Subject: [PATCH 120/842] Do not show history in more info if component not loaded --- .../frontend/www_static/polymer/dialogs/more-info-dialog.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html b/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html index f0c75c9f37e..696c47a2143 100644 --- a/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html +++ b/homeassistant/components/frontend/www_static/polymer/dialogs/more-info-dialog.html @@ -11,7 +11,9 @@
- + From 4001a2d31613ca0082ec69722cc16352cef2b445 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 May 2015 15:55:33 +0200 Subject: [PATCH 121/842] add pyown --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 59a1c3ea39a..3afa9c39a22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,3 +46,6 @@ python-pushover>=0.2 # Transmission Torrent Client transmissionrpc>=0.11 + +# OpenWeatherMap Web API +pyowm>=2.2.0 \ No newline at end of file From 70f1ec9dce684929b911dd276dbc435f50a63a40 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 May 2015 21:52:34 +0200 Subject: [PATCH 122/842] add openweathermap sensor --- .../components/sensor/openweathermap.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 homeassistant/components/sensor/openweathermap.py diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py new file mode 100644 index 00000000000..ed5520a3817 --- /dev/null +++ b/homeassistant/components/sensor/openweathermap.py @@ -0,0 +1,174 @@ +""" +homeassistant.components.sensor.openweathermap +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OpenWeatherMap (OWM) service. + +Configuration: + +To use the OpenWeatherMap sensor you will need to add something like the +following to your config/configuration.yaml + +sensor: + platform: openweathermap + api_key: YOUR_APP_KEY + monitored_variables: + - type: 'weather' + - type: 'temperature' + - type: 'wind_speed' + - type: 'humidity' + - type: 'pressure' + - type: 'clouds' + - type: 'rain' + - type: 'snow' + +VARIABLES: + +api_key +*Required +To retrieve this value log into your account at http://openweathermap.org/ + +monitored_variables +*Required +An array specifying the variables to monitor. + +These are the variables for the monitored_variables array: + +type +*Required +The variable you wish to monitor, see the configuration example above for a +list of all available variables + +Details for the API : http://bugs.openweathermap.org/projects/api/wiki + +Only metric measurements are supported at the moment. + +""" +import logging + +from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) +_THROTTLED_REFRESH = None +SENSOR_TYPES = { + 'weather': ['Condition', ''], + 'temperature': ['Temperature', ''], + 'wind_speed': ['Wind speed', 'm/s'], + 'humidity': ['Humidity', '%'], + 'pressure': ['Pressure', 'hPa'], + 'clouds': ['Cloud coverage', '%'], + 'rain': ['Rain', 'mm'], + 'snow': ['Snow', 'mm'] +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the OpenWeatherMap sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + try: + from pyowm import OWM + + except ImportError: + _LOGGER.exception( + "Unable to import pyowm. " + "Did you maybe not install the 'PyOWM' package?") + + return None + + SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit + unit = hass.config.temperature_unit + owm = OWM(config.get(CONF_API_KEY, None)) + obs = owm.weather_at_coords(hass.config.latitude, hass.config.longitude) + + if not owm: + _LOGGER.error( + "Connection error " + "Please check your settings for OpenWeatherMap.") + return None + + dev = [] + for variable in config['monitored_variables']: + if variable['type'] not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable['type']) + else: + dev.append(OpenWeatherMapSensor(variable['type'], obs, unit)) + + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class OpenWeatherMapSensor(Entity): + """ Implements an OpenWeatherMap sensor. """ + + def __init__(self, sensor_type, weather_data, unit): + self.client_name = 'Weather - ' + self._name = SENSOR_TYPES[sensor_type][0] + self.owa_client = weather_data + self._unit = unit + self.type = sensor_type + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + return self.client_name + ' ' + self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + # pylint: disable=too-many-branches + def update(self): + """ Gets the latest data from OWM and updates the states. """ + data = self.owa_client.get_weather() + + if self.type == 'weather': + self._state = data.get_detailed_status() + + if self.type == 'temperature': + if self._unit == TEMP_CELCIUS: + self._state = round(data.get_temperature('celsius')['temp'], + 1) + elif self._unit == TEMP_FAHRENHEIT: + self._state = round(data.get_temperature('fahrenheit')['temp'], + 1) + else: + self._state = round(data.get_temperature()['temp'], 1) + + elif self.type == 'wind_speed': + self._state = data.get_wind()['speed'] + + elif self.type == 'humidity': + self._state = data.get_humidity() + + elif self.type == 'pressure': + self._state = round(data.get_pressure()['press'], 0) + + elif self.type == 'clouds': + self._state = data.get_clouds() + + elif self.type == 'rain': + if data.get_rain(): + self._state = round(data.get_rain()['3h'], 0) + else: + self._state = 'not raining' + self._unit_of_measurement = '' + + elif self.type == 'snow': + if data.get_snow(): + self._state = round(data.get_snow(), 0) + else: + self._state = 'not snowing' + self._unit_of_measurement = '' From fd6de36d2ca625ce2163863f508e7d3d677fb489 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 May 2015 21:56:42 +0200 Subject: [PATCH 123/842] add openweathermap, sort the content alphabetically --- .coveragerc | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2115ee07b3c..250d207f343 100644 --- a/.coveragerc +++ b/.coveragerc @@ -22,25 +22,26 @@ omit = homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py - homeassistant/components/keyboard.py homeassistant/components/browser.py - homeassistant/components/switch/wemo.py - homeassistant/components/thermostat/nest.py - homeassistant/components/light/hue.py - homeassistant/components/sensor/systemmonitor.py - homeassistant/components/sensor/sabnzbd.py - homeassistant/components/sensor/mysensors.py - homeassistant/components/notify/pushbullet.py - homeassistant/components/notify/pushover.py - homeassistant/components/notify/instapush.py - homeassistant/components/notify/nma.py - homeassistant/components/media_player/cast.py + homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py - homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py - homeassistant/components/device_tracker/ddwrt.py + homeassistant/components/device_tracker/tomato.py + homeassistant/components/keyboard.py + homeassistant/components/light/hue.py + homeassistant/components/media_player/cast.py + homeassistant/components/notify/instapush.py + homeassistant/components/notify/nma.py + homeassistant/components/notify/pushbullet.py + homeassistant/components/notify/pushover.py + homeassistant/components/sensor/mysensors.py + homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/transmission.py + homeassistant/components/switch/wemo.py + homeassistant/components/thermostat/nest.py [report] From ca8be5015a201a0da63c2c1c3ee3dfe0cf625676 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2015 18:24:32 -0700 Subject: [PATCH 124/842] Add config API endpoint --- homeassistant/__init__.py | 11 +++++++++++ homeassistant/components/api.py | 9 +++++++++ homeassistant/const.py | 1 + 3 files changed, 21 insertions(+) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 6a7b328ba40..a132f96a8a6 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -959,6 +959,17 @@ class Config(object): # Could not convert value to float return value, unit + def as_dict(self): + """ Converts config to a dictionary. """ + return { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'temperature_unit': self.temperature_unit, + 'location_name': self.location_name, + 'time_zone': self.time_zone.zone, + 'components': self.components, + } + class HomeAssistantError(Exception): """ General Home Assistant exception occured. """ diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b5cdb9cae6c..296dc809049 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -15,6 +15,7 @@ import homeassistant.remote as rem from homeassistant.const import ( URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS, + URL_API_CONFIG, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNPROCESSABLE_ENTITY) @@ -42,6 +43,9 @@ def setup(hass, config): # /api/stream hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream) + # /api/config + hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config) + # /states hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states) hass.http.register_path( @@ -140,6 +144,11 @@ def _handle_get_api_stream(handler, path_match, data): hass.bus.remove_listener(MATCH_ALL, forward_events) +def _handle_get_api_config(handler, path_match, data): + """ Returns a dict containing Home Assistant config. """ + handler.write_json(handler.server.hass.config.as_dict()) + + 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()) diff --git a/homeassistant/const.py b/homeassistant/const.py index b85340263f0..bb2e372d22c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -111,6 +111,7 @@ SERVER_PORT = 8123 URL_ROOT = "/" URL_API = "/api/" URL_API_STREAM = "/api/stream" +URL_API_CONFIG = "/api/config" URL_API_STATES = "/api/states" URL_API_STATES_ENTITY = "/api/states/{}" URL_API_EVENTS = "/api/events" From 30f78f7fa6621be45b557378d611837f973b9aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2015 19:02:29 -0700 Subject: [PATCH 125/842] Add API endpoint to bootstrap frontend --- homeassistant/components/api.py | 39 +++++++++++++++++++++++++-------- homeassistant/const.py | 1 + 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 296dc809049..e5785903496 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -15,7 +15,7 @@ import homeassistant.remote as rem from homeassistant.const import ( URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS, - URL_API_CONFIG, + URL_API_CONFIG, URL_API_BOOTSTRAP, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNPROCESSABLE_ENTITY) @@ -46,6 +46,10 @@ def setup(hass, config): # /api/config hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config) + # /api/bootstrap + hass.http.register_path( + 'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap) + # /states hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states) hass.http.register_path( @@ -145,10 +149,22 @@ def _handle_get_api_stream(handler, path_match, data): def _handle_get_api_config(handler, path_match, data): - """ Returns a dict containing Home Assistant config. """ + """ Returns the Home Assistant config. """ handler.write_json(handler.server.hass.config.as_dict()) +def _handle_get_api_bootstrap(handler, path_match, data): + """ Returns all data needed to bootstrap Home Assistant. """ + hass = handler.server.hass + + handler.write_json({ + 'config': hass.config.as_dict(), + 'states': hass.states.all(), + 'events': _events_json(hass), + 'services': _services_json(hass), + }) + + 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()) @@ -199,9 +215,7 @@ def _handle_post_state_entity(handler, path_match, data): 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()]) + handler.write_json(_events_json(handler.server.hass)) def _handle_api_post_events_event(handler, path_match, event_data): @@ -236,10 +250,7 @@ def _handle_api_post_events_event(handler, path_match, event_data): 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()]) + handler.write_json(_services_json(handler.server.hass)) # pylint: disable=invalid-name @@ -321,3 +332,13 @@ def _handle_get_api_components(handler, path_match, data): """ Returns all the loaded components. """ handler.write_json(handler.server.hass.config.components) + +def _services_json(hass): + """ Generate services data to JSONify. """ + return [{"domain": key, "services": value} + for key, value in hass.services.services.items()] + +def _events_json(hass): + """ Generate event data to JSONify. """ + return [{"event": key, "listener_count": value} + for key, value in hass.bus.listeners.items()] diff --git a/homeassistant/const.py b/homeassistant/const.py index bb2e372d22c..cfd37576ff1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -120,6 +120,7 @@ URL_API_SERVICES = "/api/services" URL_API_SERVICES_SERVICE = "/api/services/{}/{}" URL_API_EVENT_FORWARD = "/api/event_forwarding" URL_API_COMPONENTS = "/api/components" +URL_API_BOOTSTRAP = "/api/bootstrap" HTTP_OK = 200 HTTP_CREATED = 201 From 916c30072b608f7506703652b52f8aa325e90e19 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2015 20:49:02 -0700 Subject: [PATCH 126/842] Update frontend dependencies and compile new version --- homeassistant/components/frontend/version.py | 2 +- .../components/frontend/www_static/frontend.html | 13 +++++++------ .../frontend/www_static/polymer/bower.json | 9 ++++++--- .../frontend/www_static/polymer/home-assistant-js | 2 +- .../frontend/www_static/webcomponents.min.js | 12 +++++++----- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 30b4dbae681..b760bcd8b1d 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 = "fdfcc1c10ff8713976c482931769a8e6" +VERSION = "96563b1a1f37d7679ef203e4f356b170" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index f9812693475..9bf651a82a2 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -11,11 +11,12 @@ window.PolymerGestures={},function(a){var b=!1,c=document.createElement("meta"); LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",ObjectExpression:"ObjectExpression",Program:"Program",Property:"Property",ThisExpression:"ThisExpression",UnaryExpression:"UnaryExpression"},V={UnexpectedToken:"Unexpected token %0",UnknownLabel:"Undefined label '%0'",Redeclaration:"%0 '%1' has already been declared"};var ab=H,bb=L;a.esprima={parse:R}}(this),function(a){"use strict";function b(a,b,d,e){var f;try{if(f=c(a),f.scopeIdent&&(d.nodeType!==Node.ELEMENT_NODE||"TEMPLATE"!==d.tagName||"bind"!==b&&"repeat"!==b))throw Error("as and in can only be used within