Skip to content
Snippets Groups Projects
Commit 5ec686b0 authored by Paulus Schoutsen's avatar Paulus Schoutsen
Browse files

Merge pull request #81 from balloob/dev

Update master with latest changes
parents cd60f9a8 b0bf775d
No related branches found
No related tags found
No related merge requests found
Showing
with 677 additions and 54 deletions
...@@ -27,6 +27,7 @@ omit = ...@@ -27,6 +27,7 @@ omit =
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ddwrt.py
[report] [report]
......
...@@ -6,7 +6,7 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho ...@@ -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: 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 [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 [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) * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast)
......
...@@ -22,7 +22,7 @@ from homeassistant.const import ( ...@@ -22,7 +22,7 @@ from homeassistant.const import (
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
TEMP_CELCIUS, TEMP_FAHRENHEIT) TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME)
import homeassistant.util as util import homeassistant.util as util
DOMAIN = "homeassistant" DOMAIN = "homeassistant"
...@@ -325,19 +325,23 @@ class EventOrigin(enum.Enum): ...@@ -325,19 +325,23 @@ class EventOrigin(enum.Enum):
class Event(object): class Event(object):
""" Represents an event within the Bus. """ """ Represents an event within the Bus. """
__slots__ = ['event_type', 'data', 'origin'] __slots__ = ['event_type', 'data', 'origin', 'time_fired']
def __init__(self, event_type, data=None, origin=EventOrigin.local): def __init__(self, event_type, data=None, origin=EventOrigin.local,
time_fired=None):
self.event_type = event_type self.event_type = event_type
self.data = data or {} self.data = data or {}
self.origin = origin self.origin = origin
self.time_fired = util.strip_microseconds(
time_fired or dt.datetime.now())
def as_dict(self): def as_dict(self):
""" Returns a dict representation of this Event. """ """ Returns a dict representation of this Event. """
return { return {
'event_type': self.event_type, 'event_type': self.event_type,
'data': dict(self.data), 'data': dict(self.data),
'origin': str(self.origin) 'origin': str(self.origin),
'time_fired': util.datetime_to_str(self.time_fired),
} }
def __repr__(self): def __repr__(self):
...@@ -459,7 +463,9 @@ class State(object): ...@@ -459,7 +463,9 @@ class State(object):
__slots__ = ['entity_id', 'state', 'attributes', __slots__ = ['entity_id', 'state', 'attributes',
'last_changed', 'last_updated'] 'last_changed', 'last_updated']
def __init__(self, entity_id, state, attributes=None, last_changed=None): # 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): if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError(( raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. " "Invalid entity id encountered: {}. "
...@@ -468,7 +474,7 @@ class State(object): ...@@ -468,7 +474,7 @@ class State(object):
self.entity_id = entity_id.lower() self.entity_id = entity_id.lower()
self.state = state self.state = state
self.attributes = attributes or {} 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 # Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict()) # state == State.from_dict(state.as_dict())
...@@ -482,6 +488,18 @@ class State(object): ...@@ -482,6 +488,18 @@ class State(object):
""" Returns domain of this state. """ """ Returns domain of this state. """
return util.split_entity_id(self.entity_id)[0] return util.split_entity_id(self.entity_id)[0]
@property
def object_id(self):
""" Returns object_id of this state. """
return util.split_entity_id(self.entity_id)[1]
@property
def name(self):
""" Name to represent this state. """
return (
self.attributes.get(ATTR_FRIENDLY_NAME) or
self.object_id.replace('_', ' '))
def copy(self): def copy(self):
""" Creates a copy of itself. """ """ Creates a copy of itself. """
return State(self.entity_id, self.state, return State(self.entity_id, self.state,
...@@ -494,7 +512,8 @@ class State(object): ...@@ -494,7 +512,8 @@ class State(object):
return {'entity_id': self.entity_id, return {'entity_id': self.entity_id,
'state': self.state, 'state': self.state,
'attributes': self.attributes, '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 @classmethod
def from_dict(cls, json_dict): def from_dict(cls, json_dict):
...@@ -511,8 +530,13 @@ class State(object): ...@@ -511,8 +530,13 @@ class State(object):
if last_changed: if last_changed:
last_changed = util.str_to_datetime(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'], 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): def __eq__(self, other):
return (self.__class__ == other.__class__ and return (self.__class__ == other.__class__ and
......
...@@ -157,7 +157,8 @@ class DeviceTracker(object): ...@@ -157,7 +157,8 @@ class DeviceTracker(object):
def update_devices(self, now): def update_devices(self, now):
""" Update device states based on the found devices. """ """ 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 found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices()) self.device_scanner.scan_devices())
......
""" Supports scanning a DD-WRT router. """
import logging
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__)
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
# pylint: disable=unused-argument
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 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 = (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)
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 idx in range(0, num_clients):
# get every 9th element which is the MAC address
index = idx * 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 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", response)
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 """
return {
key: val for key, val in _DDWRT_DATA_REGEX
.findall(data_str)}
""" Supports scanning using nmap. """ """ Supports scanning using nmap. """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
import threading
from collections import namedtuple from collections import namedtuple
import subprocess import subprocess
import re import re
...@@ -54,7 +53,6 @@ class NmapDeviceScanner(object): ...@@ -54,7 +53,6 @@ class NmapDeviceScanner(object):
def __init__(self, config): def __init__(self, config):
self.last_results = [] self.last_results = []
self.lock = threading.Lock()
self.hosts = config[CONF_HOSTS] self.hosts = config[CONF_HOSTS]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes) self.home_interval = timedelta(minutes=minutes)
...@@ -116,28 +114,27 @@ class NmapDeviceScanner(object): ...@@ -116,28 +114,27 @@ class NmapDeviceScanner(object):
if not self.success_init: if not self.success_init:
return False return False
with self.lock: _LOGGER.info("Scanning")
_LOGGER.info("Scanning")
options = "-F --host-timeout 5"
options = "-F" exclude_targets = set()
exclude_targets = set() if self.home_interval:
if self.home_interval: now = datetime.now()
now = datetime.now() for host in self.last_results:
for host in self.last_results: if host.last_update + self.home_interval > now:
if host.last_update + self.home_interval > now: exclude_targets.add(host)
exclude_targets.add(host) if len(exclude_targets) > 0:
if len(exclude_targets) > 0: target_list = [t.ip for t in exclude_targets]
target_list = [t.ip for t in exclude_targets] options += " --exclude {}".format(",".join(target_list))
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 nmap.rc == 0: if self._parse_results(nmap.stdout):
if self._parse_results(nmap.stdout): self.last_results.extend(exclude_targets)
self.last_results.extend(exclude_targets) else:
else: self.last_results = []
self.last_results = [] _LOGGER.error(nmap.stderr)
_LOGGER.error(nmap.stderr) return False
return False
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "a063d1482fd49e9297d64e1329324f1c" VERSION = "1e004712440afc642a44ad927559587e"
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="display-time" attributes="dateObj">
<template>
{{ time }}
</template>
<script>
(function() {
var timeFormatOptions = {hour: 'numeric', minute: '2-digit'};
Polymer({
time: "",
dateObjChanged: function(oldVal, newVal) {
if (!newVal) {
this.time = "";
}
this.time = newVal.toLocaleTimeString([], timeFormatOptions);
},
});
})();
</script>
</polymer-element>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/logbook-entry.html">
<polymer-element name="ha-logbook" attributes="entries" noscript>
<template>
<style>
.logbook {
}
</style>
<div class='logbook'>
<template repeat="{{entries as entry}}">
<logbook-entry entryObj="{{entry}}"></logbook-entry>
</template>
</div>
</template>
</polymer>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="domain-icon.html">
<link rel="import" href="display-time.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="logbook-entry" attributes="entryObj">
<template>
<core-style ref='ha-main'></core-style>
<style>
.logbook-entry {
line-height: 2em;
}
.time {
width: 55px;
font-size: .8em;
}
.icon {
margin: 0 8px 0 16px;
}
.name {
text-transform: capitalize;
}
.message {
}
</style>
<div horizontal layout class='logbook-entry'>
<display-time dateObj="{{entryObj.when}}" class='time secondary-text-color'></display-time>
<domain-icon domain="{{entryObj.domain}}" class='icon primary-text-color'></domain-icon>
<div class='message primary-text-color' flex>
<template if="{{!entryObj.entityId}}">
<span class='name'>{{entryObj.name}}</span>
</template>
<template if="{{entryObj.entityId}}">
<a href='#' on-click="{{entityClicked}}" class='name'>{{entryObj.name}}</a>
</template>
{{entryObj.message}}
</div>
</div>
</template>
<script>
(function() {
var uiActions = window.hass.uiActions;
Polymer({
entityClicked: function(ev) {
ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId);
}
});
})();
</script>
</polymer-element>
...@@ -7,12 +7,14 @@ ...@@ -7,12 +7,14 @@
<polymer-element name="more-info-dialog"> <polymer-element name="more-info-dialog">
<template> <template>
<ha-dialog id="dialog"> <ha-dialog id="dialog" on-core-overlay-open="{{dialogOpenChanged}}">
<div> <div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'> <state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content> </state-card-content>
<state-timeline stateHistory="{{stateHistory}}"></state-timeline> <state-timeline stateHistory="{{stateHistory}}"></state-timeline>
<more-info-content stateObj="{{stateObj}}"></more-info-content> <more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>
</div> </div>
</ha-dialog> </ha-dialog>
</template> </template>
...@@ -27,11 +29,16 @@ Polymer(Polymer.mixin({ ...@@ -27,11 +29,16 @@ Polymer(Polymer.mixin({
stateObj: null, stateObj: null,
stateHistory: null, stateHistory: null,
hasHistoryComponent: false, hasHistoryComponent: false,
dialogOpen: false,
observe: { observe: {
'stateObj.attributes': 'reposition' 'stateObj.attributes': 'reposition'
}, },
created: function() {
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
},
attached: function() { attached: function() {
this.listenToStores(true); this.listenToStores(true);
}, },
...@@ -66,6 +73,13 @@ Polymer(Polymer.mixin({ ...@@ -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) { changeEntityId: function(entityId) {
this.entityId = entityId; this.entityId = entityId;
......
Subproject commit e048bf6ece91983b9f03aafeb414ae5c535288a2 Subproject commit 282004e3e27134a3de1b9c0e6c264ce811f3e510
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
<link rel="import" href="../layouts/partial-states.html"> <link rel="import" href="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.html"> <link rel="import" href="../layouts/partial-history.html">
<link rel="import" href="../layouts/partial-logbook.html">
<link rel="import" href="../layouts/partial-dev-fire-event.html"> <link rel="import" href="../layouts/partial-dev-fire-event.html">
<link rel="import" href="../layouts/partial-dev-call-service.html"> <link rel="import" href="../layouts/partial-dev-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.html"> <link rel="import" href="../layouts/partial-dev-set-state.html">
...@@ -96,6 +97,13 @@ ...@@ -96,6 +97,13 @@
</paper-item> </paper-item>
</template> </template>
<template if="{{hasLogbookComponent}}">
<paper-item data-panel="logbook">
<core-icon icon="list"></core-icon>
Logbook
</paper-item>
</template>
<div flex></div> <div flex></div>
<paper-item on-click="{{handleLogOutClick}}"> <paper-item on-click="{{handleLogOutClick}}">
...@@ -136,6 +144,9 @@ ...@@ -136,6 +144,9 @@
<template if="{{selected == 'history'}}"> <template if="{{selected == 'history'}}">
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history> <partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
</template> </template>
<template if="{{selected == 'logbook'}}">
<partial-logbook main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-logbook>
</template>
<template if="{{selected == 'fire-event'}}"> <template if="{{selected == 'fire-event'}}">
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event> <partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
</template> </template>
...@@ -161,6 +172,7 @@ Polymer(Polymer.mixin({ ...@@ -161,6 +172,7 @@ Polymer(Polymer.mixin({
narrow: false, narrow: false,
activeFilters: [], activeFilters: [],
hasHistoryComponent: false, hasHistoryComponent: false,
hasLogbookComponent: false,
isStreaming: false, isStreaming: false,
hasStreamError: false, hasStreamError: false,
...@@ -185,7 +197,7 @@ Polymer(Polymer.mixin({ ...@@ -185,7 +197,7 @@ Polymer(Polymer.mixin({
componentStoreChanged: function(componentStore) { componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history'); this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasScriptComponent = componentStore.isLoaded('script'); this.hasLogbookComponent = componentStore.isLoaded('logbook');
}, },
streamStoreChanged: function(streamStore) { streamStoreChanged: function(streamStore) {
......
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/ha-logbook.html">
<polymer-element name="partial-logbook" attributes="narrow togglePanel">
<template>
<style>
.content {
background-color: white;
padding: 8px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Logbook</span>
<span header-buttons>
<paper-icon-button icon="refresh"
on-click="{{handleRefreshClick}}"></paper-icon-button>
</span>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<ha-logbook entries="{{entries}}"></ha-logbook>
</div>
</partial-base>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookActions = window.hass.logbookActions;
Polymer(Polymer.mixin({
entries: null,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
logbookStoreChanged: function(logbookStore) {
if (logbookStore.isStale()) {
logbookActions.fetch();
}
this.entries = logbookStore.all.toArray();
},
handleRefreshClick: function() {
logbookActions.fetch();
},
}, storeListenerMixIn));
</script>
</polymer>
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<link rel="import" href="more-info-thermostat.html"> <link rel="import" href="more-info-thermostat.html">
<link rel="import" href="more-info-script.html"> <link rel="import" href="more-info-script.html">
<polymer-element name="more-info-content" attributes="stateObj"> <polymer-element name="more-info-content" attributes="stateObj dialogOpen">
<template> <template>
<style> <style>
:host { :host {
...@@ -20,11 +20,20 @@ ...@@ -20,11 +20,20 @@
<script> <script>
Polymer({ Polymer({
classNames: '', classNames: '',
dialogOpen: false,
observe: { observe: {
'stateObj.attributes': 'stateAttributesChanged', 'stateObj.attributes': 'stateAttributesChanged',
}, },
dialogOpenChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
if (moreInfoContainer.lastChild) {
moreInfoContainer.lastChild.dialogOpen = newVal;
}
},
stateObjChanged: function(oldVal, newVal) { stateObjChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer; var moreInfoContainer = this.$.moreInfoContainer;
...@@ -42,10 +51,12 @@ Polymer({ ...@@ -42,10 +51,12 @@ Polymer({
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType); var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
moreInfo.stateObj = newVal; moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
moreInfoContainer.appendChild(moreInfo); moreInfoContainer.appendChild(moreInfo);
} else { } else {
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
moreInfoContainer.lastChild.stateObj = newVal; moreInfoContainer.lastChild.stateObj = newVal;
} }
......
...@@ -51,7 +51,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) { ...@@ -51,7 +51,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player": case "media_player":
var icon = "hardware:cast"; var icon = "hardware:cast";
if (state !== "idle") { if (state && state !== "idle") {
icon += "-connected"; icon += "-connected";
} }
......
<link rel="import" href="../bower_components/core-style/core-style.html"> <link rel="import" href="../bower_components/core-style/core-style.html">
<core-style id='ha-main'>
/* 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-color { color: #FF9800; }
body {
color: #212121;
}
a {
color: #FF9800;
text-decoration: none;
}
</core-style>
<core-style id='ha-animations'> <core-style id='ha-animations'>
@-webkit-keyframes ha-spin { @-webkit-keyframes ha-spin {
0% { 0% {
......
...@@ -67,6 +67,10 @@ def get_states(point_in_time, entity_ids=None, run=None): ...@@ -67,6 +67,10 @@ def get_states(point_in_time, entity_ids=None, run=None):
if run is None: if run is None:
run = recorder.run_information(point_in_time) run = recorder.run_information(point_in_time)
# History did not run before point_in_time
if run is None:
return []
where = run.where_after_start_run + "AND created < ? " where = run.where_after_start_run + "AND created < ? "
where_data = [point_in_time] where_data = [point_in_time]
......
"""
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 (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.util as util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook'
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
def setup(hass, config):
""" Listens for download events to download files. """
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
return True
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = datetime.now().date()
handler.write_json(humanify(
recorder.query_events(QUERY_EVENTS_AFTER, (start_today,))))
class Entry(object):
""" A human readable version of the log. """
# pylint: disable=too-many-arguments, too-few-public-methods
def __init__(self, when=None, name=None, message=None, domain=None,
entity_id=None):
self.when = when
self.name = name
self.message = message
self.domain = domain
self.entity_id = entity_id
def as_dict(self):
""" Convert Entry to a dict to be used within JSON. """
return {
'when': util.datetime_to_str(self.when),
'name': self.name,
'message': self.message,
'domain': self.domain,
'entity_id': self.entity_id,
}
def humanify(events):
"""
Generator that converts a list of events into Entry objects.
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
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
events,
lambda event: event.time_fired.minute // GROUP_BY_MINUTES):
events_batch = list(g_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:
entity_id = event.data['entity_id']
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:
# 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 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
# Skip all but the last sensor state
if domain == 'sensor' and \
event != last_sensor_event[to_state.entity_id]:
continue
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:
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", 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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment