From 8a14f46595b0f80f1eacc7500e70f4941a0aa913 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Jun 2015 22:56:55 -0700 Subject: [PATCH 1/2] Add support to logbook component to browse days --- homeassistant/components/logbook.py | 31 +++++--- .../sensor/swiss_public_transport.py | 2 +- homeassistant/components/sensor/time_date.py | 6 +- homeassistant/util/dt.py | 71 ++++++++++++------- 4 files changed, 72 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index cad31d41cab..d876e9002c8 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,12 +4,14 @@ homeassistant.components.logbook Parses events and generates a human log. """ +from datetime import timedelta from itertools import groupby +import re 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) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) import homeassistant.util.dt as dt_util import homeassistant.components.recorder as recorder import homeassistant.components.sun as sun @@ -17,12 +19,10 @@ import homeassistant.components.sun as sun DOMAIN = "logbook" DEPENDENCIES = ['recorder', 'http'] -URL_LOGBOOK = '/api/logbook' +URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P\d{4}-\d{2}-\d{2})|)') -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 @@ -37,11 +37,26 @@ def setup(hass, config): def _handle_get_logbook(handler, path_match, data): """ Return logbook entries. """ - start_today = dt_util.now().replace(hour=0, minute=0, second=0) + date_str = path_match.group('date') - handler.write_json(humanify( - recorder.query_events( - QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),)))) + if date_str: + start_date = dt_util.date_str_to_date(date_str) + + if start_date is None: + handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST) + return + + start_day = dt_util.start_of_local_day(start_date) + else: + start_day = dt_util.start_of_local_day() + + end_day = start_day + timedelta(days=1) + + events = recorder.query_events( + QUERY_EVENTS_BETWEEN, + (dt_util.as_utc(start_day), dt_util.as_utc(end_day))) + + handler.write_json(humanify(events)) class Entry(object): diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index a9eda2754bd..bd2d336d8ed 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -122,7 +122,7 @@ class PublicTransportData(object): try: return [ - dt_util.datetime_to_short_time_str( + dt_util.datetime_to_time_str( dt_util.as_local(dt_util.utc_from_timestamp( item['from']['departureTimestamp'])) ) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index cd35e8343ba..c77910ddcc9 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -89,9 +89,9 @@ class TimeDateSensor(Entity): """ Gets the latest data and updates the states. """ time_date = dt_util.utcnow() - time = dt_util.datetime_to_short_time_str(dt_util.as_local(time_date)) - time_utc = dt_util.datetime_to_short_time_str(time_date) - date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date)) + time = dt_util.datetime_to_time_str(dt_util.as_local(time_date)) + time_utc = dt_util.datetime_to_time_str(time_date) + date = dt_util.datetime_to_date_str(dt_util.as_local(time_date)) # Calculate the beat (Swatch Internet Time) time without date. hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':') diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index fbe00c85527..d8fecf20db8 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -9,9 +9,9 @@ import datetime as dt import pytz -DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" -DATE_SHORT_STR_FORMAT = "%Y-%m-%d" -TIME_SHORT_STR_FORMAT = "%H:%M" +DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y" +DATE_STR_FORMAT = "%Y-%m-%d" +TIME_STR_FORMAT = "%H:%M" UTC = DEFAULT_TIME_ZONE = pytz.utc @@ -34,7 +34,7 @@ def get_time_zone(time_zone_str): def utcnow(): """ Get now in UTC time. """ - return dt.datetime.now(pytz.utc) + return dt.datetime.now(UTC) def now(time_zone=None): @@ -45,12 +45,12 @@ def now(time_zone=None): def as_utc(dattim): """ Return a datetime as UTC time. Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """ - if dattim.tzinfo == pytz.utc: + if dattim.tzinfo == UTC: return dattim elif dattim.tzinfo is None: dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) - return dattim.astimezone(pytz.utc) + return dattim.astimezone(UTC) def as_local(dattim): @@ -58,17 +58,28 @@ def as_local(dattim): if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim elif dattim.tzinfo is None: - dattim = dattim.replace(tzinfo=pytz.utc) + dattim = dattim.replace(tzinfo=UTC) return dattim.astimezone(DEFAULT_TIME_ZONE) def utc_from_timestamp(timestamp): """ Returns a UTC time from a timestamp. """ - return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc) + return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) -def datetime_to_local_str(dattim, time_zone=None): +def start_of_local_day(dt_or_d=None): + """ Return local datetime object of start of day from date or datetime. """ + if dt_or_d is None: + dt_or_d = now().date() + elif isinstance(dt_or_d, dt.datetime): + dt_or_d = dt_or_d.date() + + return dt.datetime.combine(dt_or_d, dt.time()).replace( + tzinfo=DEFAULT_TIME_ZONE) + + +def datetime_to_local_str(dattim): """ Converts datetime to specified time_zone and returns a string. """ return datetime_to_str(as_local(dattim)) @@ -76,27 +87,27 @@ def datetime_to_local_str(dattim, time_zone=None): def datetime_to_str(dattim): """ Converts datetime to a string format. + @rtype : str + """ + return dattim.strftime(DATETIME_STR_FORMAT) + + +def datetime_to_time_str(dattim): + """ Converts datetime to a string containing only the time. + + @rtype : str + """ + return dattim.strftime(TIME_STR_FORMAT) + + +def datetime_to_date_str(dattim): + """ Converts datetime to a string containing only the date. + @rtype : str """ return dattim.strftime(DATE_STR_FORMAT) -def datetime_to_short_time_str(dattim): - """ Converts datetime to a string format as short time. - - @rtype : str - """ - return dattim.strftime(TIME_SHORT_STR_FORMAT) - - -def datetime_to_short_date_str(dattim): - """ Converts datetime to a string format as short date. - - @rtype : str - """ - return dattim.strftime(DATE_SHORT_STR_FORMAT) - - def str_to_datetime(dt_str): """ Converts a string to a UTC datetime object. @@ -104,7 +115,15 @@ def str_to_datetime(dt_str): """ try: return dt.datetime.strptime( - dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc) + dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc) + except ValueError: # If dt_str did not match our format + return None + + +def date_str_to_date(dt_str): + """ Converts a date string to a date object. """ + try: + return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date() except ValueError: # If dt_str did not match our format return None From 3381fff6bdabd2f982168b0662024bd2c9bd12b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Jun 2015 00:24:40 -0700 Subject: [PATCH 2/2] Frontend: logbook - allow changing displayed day --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 2710 +++++++++++++---- .../frontend/www_static/polymer/bower.json | 5 +- .../polymer/components/ha-logbook.html | 9 +- .../www_static/polymer/home-assistant-js | 2 +- .../polymer/layouts/partial-logbook.html | 67 +- .../polymer/managers/modal-manager.html | 21 + .../polymer/resources/home-assistant-js.html | 11 + homeassistant/components/logbook.py | 2 +- 9 files changed, 2155 insertions(+), 674 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 5623a8c5e7e..a9c3cd0bef2 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 = "db1ec3e116565340804da0e590058d60" +VERSION = "9472014df7b663c70bf33bb456bd8245" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 9f5c3aefe40..be9c2ba1fea 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -5948,7 +5948,7 @@ function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});va function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});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;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";Object.defineProperty(e,"__esModule",{value:!0});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;nd}},{key:"all",get:function(){return m}}]),e}(p["default"]),b=new w;b.dispatchToken=c["default"].register(function(t){switch(t.actionType){case l["default"].ACTION_NEW_LOGBOOK:g=t.date,m=new a.List(t.logbookEntries.map(function(t){return v["default"].fromJSON(t)})),y=new Date,b.emitChange();break;case l["default"].ACTION_LOG_OUT:g=null,y=null,m=new a.List,b.emitChange()}}),e["default"]=b,t.exports=e["default"]},/*!************************************!*\ !*** ./src/stores/notification.js ***! \************************************/ function(t,e,n){"use strict";function r(){return y.size}Object.defineProperty(e,"__esModule",{value:!0});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;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)})}Object.defineProperty(e,"__esModule",{value:!0});var u=function(t){return t&&t.__esModule?t:{"default":t}};e.fetchAll=i,e.fetch=o;var a=n(/*! ../call_api */5),s=u(a),c=n(/*! ../app_dispatcher */1),f=u(c),l=n(/*! ../constants */2),h=u(l),p=n(/*! ../models/state */11),_=u(p)},/*!******************************!*\ @@ -6047,6 +6047,7 @@ function(t,e,n){function r(){this._events=this._events||{},this._maxListeners=th window.hass.uiConstants = { ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO', + ACTION_SHOW_DATE_PICKER: 'ACTION_SHOW_DATE_PICKER', STATE_FILTERS: { 'group': 'Groups', @@ -6063,6 +6064,16 @@ function(t,e,n){function r(){this._events=this._events||{},this._maxListeners=th }); }, + showDatePicker: function(dateSelectedCallback, startDate) { + startDate = startDate || null; + + dispatcher.dispatch({ + actionType: window.hass.uiConstants.ACTION_SHOW_DATE_PICKER, + dateSelectedCallback: dateSelectedCallback, + startDate: startDate, + }); + }, + validateAuth: function(authToken, rememberLogin) { authActions.validate(authToken, { useStreaming: preferenceStore.useStreaming, @@ -13730,6 +13741,351 @@ is separate from validation, and `allowed-pattern` does not affect how the input }); })(); + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -22706,6 +22768,136 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN }, }); })(); + + + + + + + + + + + + + + + + + + + @@ -22801,8 +22993,13 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN }