From c3fc19353bb8c3e498f51ef1b91a20b6e16afd0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Mar 2015 22:50:20 -0700 Subject: [PATCH 01/27] Fix device tracker waiting forever when platform gets stuck --- homeassistant/components/device_tracker/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index ac0c8d483ff..6d4db7ad7ed 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -157,7 +157,8 @@ class DeviceTracker(object): def update_devices(self, now): """ Update device states based on the found devices. """ - self.lock.acquire() + if not self.lock.acquire(False): + return found_devices = set(dev.upper() for dev in self.device_scanner.scan_devices()) From 4484baa8666febec62fb0af1340626381b1a8e84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Mar 2015 22:50:51 -0700 Subject: [PATCH 02/27] Remove lock and add host timeout to NMAP scanner --- .../components/device_tracker/nmap_tracker.py | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 860ba3b45fb..b221a815fb8 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -1,7 +1,6 @@ """ Supports scanning using nmap. """ import logging from datetime import timedelta, datetime -import threading from collections import namedtuple import subprocess import re @@ -54,7 +53,6 @@ class NmapDeviceScanner(object): def __init__(self, config): self.last_results = [] - self.lock = threading.Lock() self.hosts = config[CONF_HOSTS] minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) self.home_interval = timedelta(minutes=minutes) @@ -116,28 +114,27 @@ class NmapDeviceScanner(object): if not self.success_init: return False - with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info("Scanning") - options = "-F" - exclude_targets = set() - if self.home_interval: - now = datetime.now() - for host in self.last_results: - if host.last_update + self.home_interval > now: - exclude_targets.add(host) - if len(exclude_targets) > 0: - target_list = [t.ip for t in exclude_targets] - options += " --exclude {}".format(",".join(target_list)) + options = "-F --host-timeout 5" + exclude_targets = set() + if self.home_interval: + now = datetime.now() + for host in self.last_results: + if host.last_update + self.home_interval > now: + exclude_targets.add(host) + if len(exclude_targets) > 0: + target_list = [t.ip for t in exclude_targets] + options += " --exclude {}".format(",".join(target_list)) - nmap = NmapProcess(targets=self.hosts, options=options) + nmap = NmapProcess(targets=self.hosts, options=options) - nmap.run() + nmap.run() - if nmap.rc == 0: - if self._parse_results(nmap.stdout): - self.last_results.extend(exclude_targets) - else: - self.last_results = [] - _LOGGER.error(nmap.stderr) - return False + if nmap.rc == 0: + if self._parse_results(nmap.stdout): + self.last_results.extend(exclude_targets) + else: + self.last_results = [] + _LOGGER.error(nmap.stderr) + return False From c8c38e498a54d96c7c63f219afbe33332fbf9db7 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 03:51:33 +1100 Subject: [PATCH 03/27] Added a device tracker for dd-wrt routers --- .../components/device_tracker/ddwrt.py | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 homeassistant/components/device_tracker/ddwrt.py diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py new file mode 100644 index 00000000000..a055d2d496f --- /dev/null +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -0,0 +1,159 @@ +""" Supports scanning a DD-WRT router. """ +import logging +import json +from datetime import timedelta +import re +import threading +import requests + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + + +def get_scanner(hass, config): + """ Validates config and returns a DdWrt scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = DdWrtDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +# pylint: disable=too-many-instance-attributes +class DdWrtDeviceScanner(object): + """ This class queries a wireless router running DD-WRT firmware + for connected devices. Adapted from Tomato scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + self.mac2name = None + + #test the router is accessible + url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + + return self.last_results + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + + with self.lock: + # if not initialised and not already scanned and not found + if self.mac2name is None or device not in self.mac2name: + url = 'http://{}/Status_Lan.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + + if not data: + return + + dhcp_leases = data.get('dhcp_leases', None) + if dhcp_leases: + + # remove leading and trailing single quotes + cleaned_str = dhcp_leases.strip().strip('"') + elements = cleaned_str.split('","') + num_clients = int(len(elements)/5) + self.mac2name = {} + for x in range(0, num_clients): + # this is stupid but the data is a single array + # every 5 elements represents one hosts, the MAC + # is the third element and the name is the first + mac_index = (x * 5) + 2 + if mac_index < len(elements): + self.mac2name[elements[mac_index]] = elements[x * 5] + + return self.mac2name.get(device, None) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the DdWrt router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + + url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + + if not data: + return False + + if data: + self.last_results = [] + active_clients = data.get('active_wireless', None) + if active_clients: + # This is really lame, instead of using JSON the ddwrt UI uses + # it's own data format for some reason and then regex's out + # values so I guess I have to do the same, LAME!!! + + # remove leading and trailing single quotes + clean_str = active_clients.strip().strip("'") + elements = clean_str.split("','") + + num_clients = int(len(elements)/9) + for x in range(0, num_clients): + # get every 9th element which is the MAC address + index = x * 9 + if index < len(elements): + self.last_results.append(elements[index]) + + return True + + return False + + def get_ddwrt_data(self, url): + """ Retrieve data from DD-WRT and return parsed result """ + try: + response = requests.get(url, auth=(self.username, + self.password), timeout=4) + except requests.exceptions.Timeout: + _LOGGER.exception("Connection to the router timed out") + return + if response.status_code == 200: + return _parse_ddwrt_response(response.text) + elif res.status_code == 401: + # Authentication error + _LOGGER.exception( + "Failed to authenticate, " + "please check your username and password") + return + else: + _LOGGER.error("Invalid response from ddwrt: %s", res) + + +def _parse_ddwrt_response(data_str): + """ Parse the awful DD-WRT data format, why didn't they use JSON????. + This code is a python version of how they are parsing in the JS """ + data = {} + pattern = re.compile(r'\{(\w+)::([^\}]*)\}') + for (key, val) in re.findall(pattern, data_str): + data[key] = val + + return data \ No newline at end of file From 5a0251c3cd99e7aa937ef55de64bc4d4c186cfe0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Mar 2015 23:11:07 -0700 Subject: [PATCH 04/27] ps: Fix recorder.py fetching wrong run information --- homeassistant/components/recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 84d4c1ecccd..367af7d2d73 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -86,7 +86,7 @@ def run_information(point_in_time=None): return RecorderRun() run = _INSTANCE.query( - "SELECT * FROM recorder_runs WHERE start>? AND END IS NULL OR END?", (point_in_time, point_in_time), return_value=RETURN_ONE_ROW) return RecorderRun(run) if run else None From 7e6af57186cb702a32cf5fa87557c26fa6ef0c60 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:29:45 +1100 Subject: [PATCH 05/27] FIxed some linting issues --- .../components/device_tracker/ddwrt.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index a055d2d496f..baebe484a27 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -1,6 +1,5 @@ """ Supports scanning a DD-WRT router. """ import logging -import json from datetime import timedelta import re import threading @@ -38,7 +37,7 @@ class DdWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.password = config[CONF_PASSWORD] self.lock = threading.Lock() @@ -73,20 +72,20 @@ class DdWrtDeviceScanner(object): dhcp_leases = data.get('dhcp_leases', None) if dhcp_leases: - # remove leading and trailing single quotes cleaned_str = dhcp_leases.strip().strip('"') - elements = cleaned_str.split('","') - num_clients = int(len(elements)/5) - self.mac2name = {} - for x in range(0, num_clients): + elements = cleaned_str.split('","') + num_clients = int(len(elements)/5) + self.mac2name = {} + for idx in range(0, num_clients): # this is stupid but the data is a single array # every 5 elements represents one hosts, the MAC # is the third element and the name is the first - mac_index = (x * 5) + 2 - if mac_index < len(elements): - self.mac2name[elements[mac_index]] = elements[x * 5] - + mac_index = (idx * 5) + 2 + if mac_index < len(elements): + mac = elements[mac_index] + self.mac2name[mac] = elements[idx * 5] + return self.mac2name.get(device, None) @Throttle(MIN_TIME_BETWEEN_SCANS) @@ -109,21 +108,22 @@ class DdWrtDeviceScanner(object): self.last_results = [] active_clients = data.get('active_wireless', None) if active_clients: - # This is really lame, instead of using JSON the ddwrt UI uses - # it's own data format for some reason and then regex's out - # values so I guess I have to do the same, LAME!!! - + # This is really lame, instead of using JSON the ddwrt UI + # uses it's own data format for some reason and then + # regex's out values so I guess I have to do the same, + # LAME!!! + # remove leading and trailing single quotes clean_str = active_clients.strip().strip("'") elements = clean_str.split("','") - - num_clients = int(len(elements)/9) - for x in range(0, num_clients): + + num_clients = int(len(elements)/9) + for idx in range(0, num_clients): # get every 9th element which is the MAC address - index = x * 9 - if index < len(elements): + index = idx * 9 + if index < len(elements): self.last_results.append(elements[index]) - + return True return False @@ -131,29 +131,29 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=(self.username, + response = requests.get(url, auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return if response.status_code == 200: return _parse_ddwrt_response(response.text) - elif res.status_code == 401: + elif response.status_code == 401: # Authentication error _LOGGER.exception( "Failed to authenticate, " "please check your username and password") return else: - _LOGGER.error("Invalid response from ddwrt: %s", res) + _LOGGER.error("Invalid response from ddwrt: %s", response) def _parse_ddwrt_response(data_str): - """ Parse the awful DD-WRT data format, why didn't they use JSON????. + """ Parse the awful DD-WRT data format, why didn't they use JSON????. This code is a python version of how they are parsing in the JS """ data = {} pattern = re.compile(r'\{(\w+)::([^\}]*)\}') for (key, val) in re.findall(pattern, data_str): data[key] = val - return data \ No newline at end of file + return data From fc07032d35bcccd217d02149a96d85a3338a0e31 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:43:41 +1100 Subject: [PATCH 06/27] Fixed some code formatting and added dd-wrt to the readme --- homeassistant/components/device_tracker/ddwrt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index baebe484a27..20db671fbc7 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -16,6 +16,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DdWrt scanner. """ if not validate_config(config, @@ -131,8 +132,10 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=(self.username, - self.password), timeout=4) + response = requests.get(url, auth=( + self.username, + self.password), + timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return From 05239c26f9ecfaa6a671e822ed24c2a230e53f18 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:50:24 +1100 Subject: [PATCH 07/27] Forgot to add README.md file changes to last commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f848d551adb..43c4d58a33d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: - * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com)) + * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index)) * Track and control [Philips Hue](http://meethue.com) lights * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) From a9ce12be34cb04299cd63bbdeb9075c3307592e5 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:59:12 +1100 Subject: [PATCH 08/27] Fixed travis CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 20db671fbc7..fa7d332feb8 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -132,9 +132,9 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=( - self.username, - self.password), + response = requests.get( + url, + auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") From a959c48708caaf7f22a8cd6a4e112aa37dd5fc82 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 19:17:51 +1100 Subject: [PATCH 09/27] Fixed travis another CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index fa7d332feb8..3d6af407ded 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -46,7 +46,7 @@ class DdWrtDeviceScanner(object): self.mac2name = None - #test the router is accessible + # Test the router is accessible url = 'http://{}/Status_Wireless.live.asp'.format(self.host) data = self.get_ddwrt_data(url) self.success_init = data is not None @@ -142,7 +142,7 @@ class DdWrtDeviceScanner(object): if response.status_code == 200: return _parse_ddwrt_response(response.text) elif response.status_code == 401: - # Authentication error + # Authentication error _LOGGER.exception( "Failed to authenticate, " "please check your username and password") From 522bbfb716568cc30bf0baaa05aa73194e44d1fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Mar 2015 13:41:07 -0700 Subject: [PATCH 10/27] Expose to more info content if dialog is open --- .../polymer/dialogs/more-info-dialog.html | 18 ++++++++++++++++-- .../polymer/more-infos/more-info-content.html | 13 ++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) 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 05a183bfd1b..98c88b7db35 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 @@ -7,12 +7,14 @@ @@ -27,11 +29,16 @@ Polymer(Polymer.mixin({ stateObj: null, stateHistory: null, hasHistoryComponent: false, + dialogOpen: false, observe: { 'stateObj.attributes': 'reposition' }, + created: function() { + this.dialogOpenChanged = this.dialogOpenChanged.bind(this); + }, + attached: function() { this.listenToStores(true); }, @@ -66,6 +73,13 @@ Polymer(Polymer.mixin({ } }, + dialogOpenChanged: function(ev) { + // we get CustomEvent, undefined and true/false from polymer… + if (typeof ev === 'object') { + this.dialogOpen = ev.detail; + } + }, + changeEntityId: function(entityId) { this.entityId = entityId; diff --git a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html index a06dd06c93f..b80a016686b 100644 --- a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html +++ b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-content.html @@ -8,7 +8,7 @@ - + +
Streaming updates
Developer Tools
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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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 }}