diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 5b3e4fd8b9e..6a7b328ba40 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -12,7 +12,6 @@ import logging import threading import enum import re -import datetime as dt import functools as ft from homeassistant.const import ( @@ -22,6 +21,7 @@ from homeassistant.const import ( EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) import homeassistant.util as util +import homeassistant.util.dt as date_util DOMAIN = "homeassistant" @@ -107,7 +107,20 @@ class HomeAssistant(object): def track_point_in_time(self, action, point_in_time): """ - Adds a listener that fires once at or after a spefic point in time. + Adds a listener that fires once after a spefic point in time. + """ + utc_point_in_time = date_util.as_utc(point_in_time) + + @ft.wraps(action) + def utc_converter(utc_now): + """ Converts passed in UTC now to local now. """ + action(date_util.as_local(utc_now)) + + self.track_point_in_utc_time(utc_converter, utc_point_in_time) + + def track_point_in_utc_time(self, action, point_in_time): + """ + Adds a listener that fires once after a specific point in UTC time. """ @ft.wraps(action) @@ -133,11 +146,19 @@ class HomeAssistant(object): self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) return point_in_time_listener + # pylint: disable=too-many-arguments + def track_utc_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None): + """ Adds a listener that will fire if time matches a pattern. """ + self.track_time_change( + action, year, month, day, hour, minute, second, utc=True) + # pylint: disable=too-many-arguments def track_time_change(self, action, year=None, month=None, day=None, - hour=None, minute=None, second=None): - """ Adds a listener that will fire if time matches a pattern. """ + hour=None, minute=None, second=None, utc=False): + """ Adds a listener that will fire if UTC time matches a pattern. """ # We do not have to wrap the function with time pattern matching logic # if no pattern given @@ -153,6 +174,9 @@ class HomeAssistant(object): """ Listens for matching time_changed events. """ now = event.data[ATTR_NOW] + if not utc: + now = date_util.as_local(now) + mat = _matcher if mat(now.year, year) and \ @@ -303,7 +327,7 @@ def create_worker_pool(): for start, job in current_jobs: _LOGGER.warning("WorkerPool:Current job from %s: %s", - util.datetime_to_str(start), job) + date_util.datetime_to_local_str(start), job) return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback) @@ -331,7 +355,7 @@ class Event(object): self.data = data or {} self.origin = origin self.time_fired = util.strip_microseconds( - time_fired or dt.datetime.now()) + time_fired or date_util.utcnow()) def as_dict(self): """ Returns a dict representation of this Event. """ @@ -339,7 +363,7 @@ class Event(object): 'event_type': self.event_type, 'data': dict(self.data), 'origin': str(self.origin), - 'time_fired': util.datetime_to_str(self.time_fired), + 'time_fired': date_util.datetime_to_str(self.time_fired), } def __repr__(self): @@ -352,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 @@ -374,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 @@ -472,13 +506,14 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = last_updated or dt.datetime.now() + 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()) # This behavior occurs because to_dict uses datetime_to_str # which does not preserve microseconds - self.last_changed = util.strip_microseconds( + self.last_changed = date_util.strip_microseconds( last_changed or self.last_updated) @property @@ -510,8 +545,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_updated': util.datetime_to_str(self.last_updated)} + 'last_changed': date_util.datetime_to_str(self.last_changed), + 'last_updated': date_util.datetime_to_str(self.last_updated)} @classmethod def from_dict(cls, json_dict): @@ -526,12 +561,12 @@ class State(object): last_changed = json_dict.get('last_changed') if last_changed: - last_changed = util.str_to_datetime(last_changed) + last_changed = date_util.str_to_datetime(last_changed) last_updated = json_dict.get('last_updated') if last_updated: - last_updated = util.str_to_datetime(last_updated) + last_updated = date_util.str_to_datetime(last_updated) return cls(json_dict['entity_id'], json_dict['state'], json_dict.get('attributes'), last_changed, last_updated) @@ -548,7 +583,7 @@ class State(object): return "".format( self.entity_id, self.state, attr, - util.datetime_to_str(self.last_changed)) + date_util.datetime_to_local_str(self.last_changed)) class StateMachine(object): @@ -585,7 +620,7 @@ class StateMachine(object): """ Returns all states that have been changed since point_in_time. """ - point_in_time = util.strip_microseconds(point_in_time) + point_in_time = date_util.strip_microseconds(point_in_time) with self._lock: return [state for state in self._states.values() @@ -847,7 +882,7 @@ class Timer(threading.Thread): last_fired_on_second = -1 - calc_now = dt.datetime.now + calc_now = date_util.utcnow interval = self.interval while not self._stop_event.isSet(): @@ -873,7 +908,13 @@ 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(): + 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/bootstrap.py b/homeassistant/bootstrap.py index 4f323f34bea..2544ff5fa79 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -15,6 +15,7 @@ from collections import defaultdict import homeassistant import homeassistant.util as util +import homeassistant.util.dt as date_util import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components @@ -183,13 +184,27 @@ def process_ha_core_config(hass, config): """ Processes the [homeassistant] section from the config. """ hac = hass.config + def set_time_zone(time_zone_str): + """ Helper method to set time zone in HA. """ + if time_zone_str is None: + return + + time_zone = date_util.get_time_zone(time_zone_str) + + if time_zone: + hac.time_zone = time_zone + date_util.set_default_time_zone(time_zone) + else: + _LOGGER.error("Received invalid time zone %s", time_zone_str) + for key, attr in ((CONF_LATITUDE, 'latitude'), (CONF_LONGITUDE, 'longitude'), - (CONF_NAME, 'location_name'), - (CONF_TIME_ZONE, 'time_zone')): + (CONF_NAME, 'location_name')): if key in config: setattr(hac, attr, config[key]) + set_time_zone(config.get(CONF_TIME_ZONE)) + for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) @@ -202,7 +217,8 @@ def process_ha_core_config(hass, config): hac.temperature_unit = TEMP_FAHRENHEIT # If we miss some of the needed values, auto detect them - if None not in (hac.latitude, hac.longitude, hac.temperature_unit): + if None not in ( + hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): return _LOGGER.info('Auto detecting location and temperature unit') @@ -227,7 +243,7 @@ def process_ha_core_config(hass, config): hac.location_name = info.city if hac.time_zone is None: - hac.time_zone = info.time_zone + set_time_zone(info.time_zone) def _ensure_loader_prepared(hass): diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6d4db7ad7ed..40dd6e5150e 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -8,11 +8,12 @@ import logging import threading import os import csv -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.loader import get_component from homeassistant.helpers import validate_config import homeassistant.util as util +import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, @@ -113,7 +114,7 @@ class DeviceTracker(object): """ Reload known devices file. """ self._read_known_devices_file() - self.update_devices(datetime.now()) + self.update_devices(dt_util.utcnow()) dev_group.update_tracked_entity_ids(self.device_entity_ids) @@ -125,7 +126,7 @@ class DeviceTracker(object): seconds = range(0, 60, seconds) _LOGGER.info("Device tracker interval second=%s", seconds) - hass.track_time_change(update_device_state, second=seconds) + hass.track_utc_time_change(update_device_state, second=seconds) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, @@ -226,7 +227,7 @@ class DeviceTracker(object): self.untracked_devices.clear() with open(known_dev_path) as inp: - default_last_seen = datetime(1990, 1, 1) + default_last_seen = dt_util.utcnow().replace(year=1990) # To track which devices need an entity_id assigned need_entity_id = [] diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index bf7aed7c7fd..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 = "93774c7a1643c7e3f9cbbb1554b36683" +VERSION = "fdfcc1c10ff8713976c482931769a8e6" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 5995df6e07d..f9812693475 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -11,11 +11,11 @@ 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 diff --git a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-js.html b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-js.html index 8d7cedcd778..6bd24e8767e 100644 --- a/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-js.html +++ b/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-js.html @@ -7,18 +7,6 @@ 'light', 'group', 'sun', 'configurator', 'thermostat', 'script' ]; - // Register some polymer filters - - PolymerExpressions.prototype.HATimeToDate = function(timeString) { - if (!timeString) return; - - return window.hass.util.parseDateTime(timeString); - }; - - PolymerExpressions.prototype.HATimeStripDate = function(timeString) { - return (timeString || "").split(' ')[0]; - }; - // Add some frontend specific helpers to the models Object.defineProperties(window.hass.stateModel.prototype, { // how to render the card for this state @@ -81,3 +69,5 @@ window.hass.uiUtil = {}; })(); + + 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 @@ --> + + diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index a288d5471d9..14be60fa97e 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -5,10 +5,11 @@ homeassistant.components.history Provide pre-made queries on top of the recorder component. """ import re -from datetime import datetime, timedelta +from datetime import timedelta from itertools import groupby from collections import defaultdict +import homeassistant.util.dt as date_util import homeassistant.components.recorder as recorder DOMAIN = 'history' @@ -22,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, )) @@ -30,7 +31,7 @@ def last_5_states(entity_id): def state_changes_during_period(start_time, end_time=None, entity_id=None): """ - Return states changes during period start_time - end_time. + Return states changes during UTC period start_time - end_time. """ where = "last_changed=last_updated AND last_changed > ? " data = [start_time] @@ -64,17 +65,17 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None): return result -def get_states(point_in_time, entity_ids=None, run=None): +def get_states(utc_point_in_time, entity_ids=None, run=None): """ Returns the states at a specific point in time. """ if run is None: - run = recorder.run_information(point_in_time) + run = recorder.run_information(utc_point_in_time) - # History did not run before point_in_time + # History did not run before utc_point_in_time if run is None: return [] where = run.where_after_start_run + "AND created < ? " - where_data = [point_in_time] + where_data = [utc_point_in_time] if entity_ids is not None: where += "AND entity_id IN ({}) ".format( @@ -93,9 +94,9 @@ def get_states(point_in_time, entity_ids=None, run=None): return recorder.query_states(query, where_data) -def get_state(point_in_time, entity_id, run=None): +def get_state(utc_point_in_time, entity_id, run=None): """ Return a state at a specific point in time. """ - states = get_states(point_in_time, (entity_id,), run) + states = get_states(utc_point_in_time, (entity_id,), run) return states[0] if states else None @@ -128,7 +129,7 @@ def _api_last_5_states(handler, path_match, data): def _api_history_period(handler, path_match, data): """ Return history over a period of time. """ # 1 day for now.. - start_time = datetime.now() - timedelta(seconds=86400) + start_time = date_util.utcnow() - timedelta(seconds=86400) entity_id = data.get('filter_entity_id') diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index a8299fbd6ed..68718a7ac43 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,14 +4,13 @@ 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.util.dt as dt_util import homeassistant.components.recorder as recorder import homeassistant.components.sun as sun @@ -38,10 +37,11 @@ def setup(hass, config): def _handle_get_logbook(handler, path_match, data): """ Return logbook entries. """ - start_today = datetime.now().date() + start_today = dt_util.now().replace(hour=0, minute=0, second=0) handler.write_json(humanify( - recorder.query_events(QUERY_EVENTS_AFTER, (start_today,)))) + recorder.query_events( + QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),)))) class Entry(object): @@ -60,7 +60,7 @@ class Entry(object): def as_dict(self): """ Convert Entry to a dict to be used within JSON. """ return { - 'when': util.datetime_to_str(self.when), + 'when': dt_util.datetime_to_str(self.when), 'name': self.name, 'message': self.message, 'domain': self.domain, diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 6856ce4d7b5..717b6514bb4 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -10,11 +10,11 @@ import threading import queue import sqlite3 from datetime import datetime, date -import time import json import atexit from homeassistant import Event, EventOrigin, State +import homeassistant.util.dt as date_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, @@ -60,8 +60,9 @@ 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]), - datetime.fromtimestamp(row[5])) + row[1], row[2], json.loads(row[3]), + date_util.utc_from_timestamp(row[4]), + date_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) @@ -72,7 +73,7 @@ def row_to_event(row): """ Convert a databse row to an event. """ try: return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()], - datetime.fromtimestamp(row[5])) + date_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to event: %s", row) @@ -113,10 +114,10 @@ class RecorderRun(object): self.start = _INSTANCE.recording_start self.closed_incorrect = False else: - self.start = datetime.fromtimestamp(row[1]) + self.start = date_util.utc_from_timestamp(row[1]) if row[2] is not None: - self.end = datetime.fromtimestamp(row[2]) + self.end = date_util.utc_from_timestamp(row[2]) self.closed_incorrect = bool(row[3]) @@ -166,7 +167,8 @@ class Recorder(threading.Thread): self.queue = queue.Queue() self.quit_object = object() self.lock = threading.Lock() - self.recording_start = datetime.now() + self.recording_start = date_util.utcnow() + self.utc_offset = date_util.now().utcoffset().total_seconds() def start_recording(event): """ Start recording. """ @@ -187,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: @@ -197,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 @@ -209,31 +214,33 @@ class Recorder(threading.Thread): def record_state(self, entity_id, state): """ Save a state to the database. """ - now = datetime.now() + now = date_util.utcnow() + # State got deleted if state is None: info = (entity_id, '', "{}", now, now, now) else: info = ( entity_id.lower(), state.state, json.dumps(state.attributes), - state.last_changed, state.last_updated, now) + state.last_changed, state.last_updated, now, self.utc_offset) self.query( "INSERT INTO states (" "entity_id, state, attributes, last_changed, last_updated," - "created) VALUES (?, ?, ?, ?, ?, ?)", info) + "created, utc_offset) VALUES (?, ?, ?, ?, ?, ?, ?)", info) def record_event(self, event): """ Save an event to the database. """ info = ( event.event_type, json.dumps(event.data, cls=JSONEncoder), - str(event.origin), datetime.now(), event.time_fired, + str(event.origin), date_util.utcnow(), event.time_fired, + self.utc_offset ) self.query( "INSERT INTO events (" - "event_type, event_data, origin, created, time_fired" - ") VALUES (?, ?, ?, ?, ?)", info) + "event_type, event_data, origin, created, time_fired, utc_offset" + ") VALUES (?, ?, ?, ?, ?, ?)", info) def query(self, sql_query, data=None, return_value=None): """ Query the database. """ @@ -262,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) @@ -282,7 +293,7 @@ class Recorder(threading.Thread): def save_migration(migration_id): """ Save and commit a migration to the database. """ cur.execute('INSERT INTO schema_version VALUES (?, ?)', - (migration_id, datetime.now())) + (migration_id, date_util.utcnow())) self.conn.commit() _LOGGER.info("Database migrated to version %d", migration_id) @@ -297,7 +308,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, @@ -306,7 +317,7 @@ class Recorder(threading.Thread): created integer) """) - cur.execute(""" + self.query(""" CREATE TABLE events ( event_id integer primary key, event_type text, @@ -314,10 +325,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, @@ -327,20 +338,44 @@ 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 + + self.query(""" + ALTER TABLE recorder_runs + ADD COLUMN utc_offset integer + """) + + self.query(""" + ALTER TABLE events + ADD COLUMN utc_offset integer + """) + + self.query(""" + ALTER TABLE states + ADD COLUMN utc_offset integer + """) + + 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) + def _close_connection(self): """ Close connection to the database. """ _LOGGER.info("Closing database") @@ -357,18 +392,18 @@ class Recorder(threading.Thread): self.query( "INSERT INTO recorder_runs (start, created) VALUES (?, ?)", - (self.recording_start, datetime.now())) + (self.recording_start, date_util.utcnow())) def _close_run(self): """ Save end time for current run. """ self.query( "UPDATE recorder_runs SET end=? WHERE start=?", - (datetime.now(), self.recording_start)) + (date_util.utcnow(), self.recording_start)) def _adapt_datetime(datetimestamp): """ Turn a datetime into an integer for in the DB. """ - return time.mktime(datetimestamp.timetuple()) + return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() def _verify_instance(): diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index a7228a1e42f..208d17167c6 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -30,7 +30,7 @@ except ImportError: # Error will be raised during setup ephem = None -from homeassistant.util import str_to_datetime, datetime_to_str +import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener @@ -55,13 +55,21 @@ def is_on(hass, entity_id=None): def next_setting(hass, entity_id=None): - """ Returns the datetime object representing the next sun setting. """ + """ Returns the local datetime object of the next sun setting. """ + utc_next = next_setting_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_setting_utc(hass, entity_id=None): + """ Returns the UTC datetime object of the next sun setting. """ entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) try: - return str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING]) + return dt_util.str_to_datetime( + state.attributes[STATE_ATTR_NEXT_SETTING]) except (AttributeError, KeyError): # AttributeError if state is None # KeyError if STATE_ATTR_NEXT_SETTING does not exist @@ -69,13 +77,21 @@ def next_setting(hass, entity_id=None): def next_rising(hass, entity_id=None): - """ Returns the datetime object representing the next sun rising. """ + """ Returns the local datetime object of the next sun rising. """ + utc_next = next_rising_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_rising_utc(hass, entity_id=None): + """ Returns the UTC datetime object of the next sun rising. """ entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) try: - return str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) + return dt_util.str_to_datetime( + state.attributes[STATE_ATTR_NEXT_RISING]) except (AttributeError, KeyError): # AttributeError if state is None # KeyError if STATE_ATTR_NEXT_RISING does not exist @@ -94,15 +110,15 @@ def setup(hass, config): logger.error("Latitude or longitude not set in Home Assistant config") return False - sun = Sun(hass, str(hass.config.latitude), str(hass.config.longitude)) - try: - sun.point_in_time_listener(datetime.now()) + sun = Sun(hass, str(hass.config.latitude), str(hass.config.longitude)) except ValueError: # Raised when invalid latitude or longitude is given to Observer logger.exception("Invalid value for latitude or longitude") return False + sun.point_in_time_listener(dt_util.utcnow()) + return True @@ -113,8 +129,11 @@ class Sun(Entity): def __init__(self, hass, latitude, longitude): self.hass = hass - self.latitude = latitude - self.longitude = longitude + self.observer = ephem.Observer() + # pylint: disable=assigning-non-slot + self.observer.lat = latitude + # pylint: disable=assigning-non-slot + self.observer.long = longitude self._state = self.next_rising = self.next_setting = None @@ -137,8 +156,8 @@ class Sun(Entity): @property def state_attributes(self): return { - STATE_ATTR_NEXT_RISING: datetime_to_str(self.next_rising), - STATE_ATTR_NEXT_SETTING: datetime_to_str(self.next_setting) + STATE_ATTR_NEXT_RISING: dt_util.datetime_to_str(self.next_rising), + STATE_ATTR_NEXT_SETTING: dt_util.datetime_to_str(self.next_setting) } @property @@ -146,22 +165,19 @@ class Sun(Entity): """ Returns the datetime when the next change to the state is. """ return min(self.next_rising, self.next_setting) - def update_as_of(self, point_in_time): - """ Calculate sun state at a point in time. """ - utc_offset = datetime.utcnow() - datetime.now() - utc_now = point_in_time + utc_offset - + def update_as_of(self, utc_point_in_time): + """ Calculate sun state at a point in UTC time. """ sun = ephem.Sun() # pylint: disable=no-member - # Setting invalid latitude and longitude to observer raises ValueError - observer = ephem.Observer() - observer.lat = self.latitude # pylint: disable=assigning-non-slot - observer.long = self.longitude # pylint: disable=assigning-non-slot + # pylint: disable=assigning-non-slot + self.observer.date = ephem.date(utc_point_in_time) - self.next_rising = ephem.localtime( - observer.next_rising(sun, start=utc_now)) - self.next_setting = ephem.localtime( - observer.next_setting(sun, start=utc_now)) + self.next_rising = self.observer.next_rising( + sun, + start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC) + self.next_setting = self.observer.next_setting( + sun, + start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC) def point_in_time_listener(self, now): """ Called when the state of the sun has changed. """ @@ -169,8 +185,9 @@ class Sun(Entity): self.update_ha_state() # Schedule next update at next_change+1 second so sun state has changed - self.hass.track_point_in_time(self.point_in_time_listener, - self.next_change + timedelta(seconds=1)) + self.hass.track_point_in_utc_time( + self.point_in_time_listener, + self.next_change + timedelta(seconds=1)) def create_event_listener(schedule, event_listener_data): diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 43d48898303..18c68808e94 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -5,9 +5,9 @@ homeassistant.helpers.state Helpers that help with state related things. """ import logging -from datetime import datetime from homeassistant import State +import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) @@ -26,7 +26,7 @@ class TrackStates(object): self.states = [] def __enter__(self): - self.now = datetime.now() + self.now = dt_util.utcnow() return self.states def __exit__(self, exc_type, exc_value, traceback): diff --git a/homeassistant/util.py b/homeassistant/util/__init__.py similarity index 94% rename from homeassistant/util.py rename to homeassistant/util/__init__.py index e50ac3f30cf..5c69fd02243 100644 --- a/homeassistant/util.py +++ b/homeassistant/util/__init__.py @@ -8,7 +8,7 @@ import collections from itertools import chain import threading import queue -from datetime import datetime, timedelta +from datetime import datetime import re import enum import socket @@ -18,12 +18,17 @@ from functools import wraps import requests +# DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package +# pylint: disable=unused-import +from .dt import ( # noqa + datetime_to_str, str_to_datetime, strip_microseconds, + datetime_to_local_str, utcnow) + + RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+') -DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" - def sanitize_filename(filename): """ Sanitizes a filename by removing .. / and \\. """ @@ -42,33 +47,6 @@ def slugify(text): return RE_SLUGIFY.sub("", text) -def datetime_to_str(dattim): - """ Converts datetime to a string format. - - @rtype : str - """ - return dattim.strftime(DATE_STR_FORMAT) - - -def str_to_datetime(dt_str): - """ Converts a string to a datetime object. - - @rtype: datetime - """ - try: - return datetime.strptime(dt_str, DATE_STR_FORMAT) - except ValueError: # If dt_str did not match our format - return None - - -def strip_microseconds(dattim): - """ Returns a copy of dattime object but with microsecond set to 0. """ - if dattim.microsecond: - return dattim - timedelta(microseconds=dattim.microsecond) - else: - return dattim - - def split_entity_id(entity_id): """ Splits a state entity_id into domain, object_id. """ return entity_id.split(".", 1) @@ -81,7 +59,7 @@ def repr_helper(inp): repr_helper(key)+"="+repr_helper(item) for key, item in inp.items()) elif isinstance(inp, datetime): - return datetime_to_str(inp) + return datetime_to_local_str(inp) else: return str(inp) @@ -464,7 +442,7 @@ class ThreadPool(object): return # Add to current running jobs - job_log = (datetime.now(), job) + job_log = (utcnow(), job) self.current_jobs.append(job_log) # Do the job diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py new file mode 100644 index 00000000000..2439f77c558 --- /dev/null +++ b/homeassistant/util/dt.py @@ -0,0 +1,96 @@ +""" +homeassistant.util.dt +~~~~~~~~~~~~~~~~~~~~~ + +Provides helper methods to handle the time in HA. + +""" +import datetime as dt + +import pytz + +DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" +UTC = DEFAULT_TIME_ZONE = pytz.utc + + +def set_default_time_zone(time_zone): + """ Sets a default time zone to be used when none is specified. """ + global DEFAULT_TIME_ZONE # pylint: disable=global-statement + + assert isinstance(time_zone, dt.tzinfo) + + DEFAULT_TIME_ZONE = time_zone + + +def get_time_zone(time_zone_str): + """ Get time zone from string. Return None if unable to determine. """ + try: + return pytz.timezone(time_zone_str) + except pytz.exceptions.UnknownTimeZoneError: + return None + + +def utcnow(): + """ Get now in UTC time. """ + return dt.datetime.now(pytz.utc) + + +def now(time_zone=None): + """ Get now in specified time zone. """ + return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE) + + +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: + return dattim + elif dattim.tzinfo is None: + dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) + + return dattim.astimezone(pytz.utc) + + +def as_local(dattim): + """ Converts a UTC datetime object to local time_zone. """ + if dattim.tzinfo == DEFAULT_TIME_ZONE: + return dattim + elif dattim.tzinfo is None: + dattim = dattim.replace(tzinfo=pytz.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) + + +def datetime_to_local_str(dattim, time_zone=None): + """ Converts datetime to specified time_zone and returns a string. """ + return datetime_to_str(as_local(dattim)) + + +def datetime_to_str(dattim): + """ Converts datetime to a string format. + + @rtype : str + """ + return dattim.strftime(DATE_STR_FORMAT) + + +def str_to_datetime(dt_str): + """ Converts a string to a UTC datetime object. + + @rtype: datetime + """ + try: + return dt.datetime.strptime( + dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc) + except ValueError: # If dt_str did not match our format + return None + + +def strip_microseconds(dattim): + """ Returns a copy of dattime object but with microsecond set to 0. """ + return dattim.replace(microsecond=0) diff --git a/requirements.txt b/requirements.txt index e1b0a942f80..59a1c3ea39a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # required for Home Assistant core requests>=2.0 pyyaml>=3.11 +pytz>=2015.2 # optional, needed for specific components diff --git a/scripts/run_tests b/scripts/run_tests index 8d1c6aed114..75b25ca805a 100755 --- a/scripts/run_tests +++ b/scripts/run_tests @@ -3,4 +3,8 @@ if [ ${PWD##*/} == "scripts" ]; then cd .. fi -python3 -m unittest discover tests +if [ "$1" = "coverage" ]; then + coverage run -m unittest discover tests +else + python3 -m unittest discover tests +fi 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..c6799defe21 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,10 +5,15 @@ 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, + EVENT_STATE_CHANGED) +from homeassistant.components import sun def get_test_config_dir(): @@ -16,10 +21,20 @@ 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 return hass @@ -37,6 +52,44 @@ 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)}) + + +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_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_scanner.py b/tests/test_component_device_tracker.py similarity index 96% rename from tests/test_component_device_scanner.py rename to tests/test_component_device_tracker.py index 2bd392c21d0..038b2363e7b 100644 --- a/tests/test_component_device_scanner.py +++ b/tests/test_component_device_tracker.py @@ -1,17 +1,18 @@ """ -tests.test_component_group -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +tests.test_component_device_tracker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests the group compoments. +Tests the device tracker compoments. """ # pylint: disable=protected-access,too-many-public-methods import unittest -from datetime import datetime, timedelta +from datetime import timedelta import logging import os import homeassistant as ha import homeassistant.loader as loader +import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker @@ -80,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') @@ -116,7 +119,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): dev2 = device_tracker.ENTITY_ID_FORMAT.format('device_2') dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3') - now = datetime.now() + now = dt_util.utcnow() # Device scanner scans every 12 seconds. We need to sync our times to # be every 12 seconds or else the time_changed event will be ignored. diff --git a/tests/test_component_history.py b/tests/test_component_history.py new file mode 100644 index 00000000000..b6ae8dab33f --- /dev/null +++ b/tests/test_component_history.py @@ -0,0 +1,137 @@ +""" +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 + +SERVER_PORT = 8126 + + +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: {http.CONF_SERVER_PORT: SERVER_PORT}}) + 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)) diff --git a/tests/test_component_logbook.py b/tests/test_component_logbook.py new file mode 100644 index 00000000000..2f8f6b8c513 --- /dev/null +++ b/tests/test_component_logbook.py @@ -0,0 +1,98 @@ +""" +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 + +SERVER_PORT = 8127 + + +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, { + http.DOMAIN: {http.CONF_SERVER_PORT: SERVER_PORT}}) + 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) 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_component_sun.py b/tests/test_component_sun.py index a4ff19429f3..aec97ede6a8 100644 --- a/tests/test_component_sun.py +++ b/tests/test_component_sun.py @@ -6,11 +6,12 @@ Tests Sun component. """ # pylint: disable=too-many-public-methods,protected-access import unittest -import datetime as dt +from datetime import timedelta import ephem import homeassistant as ha +import homeassistant.util.dt as dt_util import homeassistant.components.sun as sun @@ -42,22 +43,20 @@ class TestSun(unittest.TestCase): observer.lat = '32.87336' # pylint: disable=assigning-non-slot observer.long = '117.22743' # pylint: disable=assigning-non-slot - utc_now = dt.datetime.utcnow() + utc_now = dt_util.utcnow() body_sun = ephem.Sun() # pylint: disable=no-member - next_rising_dt = ephem.localtime( - observer.next_rising(body_sun, start=utc_now)) - next_setting_dt = ephem.localtime( - observer.next_setting(body_sun, start=utc_now)) + next_rising_dt = observer.next_rising( + body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC) + next_setting_dt = observer.next_setting( + body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC) # Home Assistant strips out microseconds # strip it out of the datetime objects - next_rising_dt = next_rising_dt - dt.timedelta( - microseconds=next_rising_dt.microsecond) - next_setting_dt = next_setting_dt - dt.timedelta( - microseconds=next_setting_dt.microsecond) + next_rising_dt = dt_util.strip_microseconds(next_rising_dt) + next_setting_dt = dt_util.strip_microseconds(next_setting_dt) - self.assertEqual(next_rising_dt, sun.next_rising(self.hass)) - self.assertEqual(next_setting_dt, sun.next_setting(self.hass)) + self.assertEqual(next_rising_dt, sun.next_rising_utc(self.hass)) + self.assertEqual(next_setting_dt, sun.next_setting_utc(self.hass)) # Point it at a state without the proper attributes self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) @@ -84,7 +83,7 @@ class TestSun(unittest.TestCase): self.assertIsNotNone(test_time) self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)}) + {ha.ATTR_NOW: test_time + timedelta(seconds=5)}) self.hass.pool.block_till_done() diff --git a/tests/test_core.py b/tests/test_core.py index a5c37f753b9..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. """ @@ -72,7 +76,7 @@ class TestHomeAssistant(unittest.TestCase): runs = [] - self.hass.track_point_in_time( + self.hass.track_point_in_utc_time( lambda x: runs.append(1), birthday_paulus) self._send_time_changed(before_birthday) @@ -88,7 +92,7 @@ class TestHomeAssistant(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) - self.hass.track_point_in_time( + self.hass.track_point_in_utc_time( lambda x: runs.append(1), birthday_paulus) self._send_time_changed(after_birthday) diff --git a/tests/test_util.py b/tests/test_util.py index 038db227e1a..f75b6db8aeb 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -35,17 +35,6 @@ class TestUtil(unittest.TestCase): self.assertEqual("Test_More", util.slugify("Test More")) self.assertEqual("Test_More", util.slugify("Test_(More)")) - def test_datetime_to_str(self): - """ Test datetime_to_str. """ - self.assertEqual("12:00:00 09-07-1986", - util.datetime_to_str(datetime(1986, 7, 9, 12, 0, 0))) - - def test_str_to_datetime(self): - """ Test str_to_datetime. """ - self.assertEqual(datetime(1986, 7, 9, 12, 0, 0), - util.str_to_datetime("12:00:00 09-07-1986")) - self.assertIsNone(util.str_to_datetime("not a datetime string")) - def test_split_entity_id(self): """ Test split_entity_id. """ self.assertEqual(['domain', 'object_id'], diff --git a/tests/test_util_dt.py b/tests/test_util_dt.py new file mode 100644 index 00000000000..5deafb58040 --- /dev/null +++ b/tests/test_util_dt.py @@ -0,0 +1,137 @@ +""" +tests.test_util +~~~~~~~~~~~~~~~~~ + +Tests Home Assistant date util methods. +""" +# pylint: disable=too-many-public-methods +import unittest +from datetime import datetime, timedelta + +import homeassistant.util.dt as dt_util + +TEST_TIME_ZONE = 'America/Los_Angeles' + + +class TestDateUtil(unittest.TestCase): + """ Tests util date methods. """ + + def setUp(self): + self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE + + def tearDown(self): + dt_util.set_default_time_zone(self.orig_default_time_zone) + + def test_get_time_zone_retrieves_valid_time_zone(self): + """ Test getting a time zone. """ + time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) + + self.assertIsNotNone(time_zone) + self.assertEqual(TEST_TIME_ZONE, time_zone.zone) + + def test_get_time_zone_returns_none_for_garbage_time_zone(self): + """ Test getting a non existing time zone. """ + time_zone = dt_util.get_time_zone("Non existing time zone") + + self.assertIsNone(time_zone) + + def test_set_default_time_zone(self): + """ Test setting default time zone. """ + time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) + + dt_util.set_default_time_zone(time_zone) + + # We cannot compare the timezones directly because of DST + self.assertEqual(time_zone.zone, dt_util.now().tzinfo.zone) + + def test_utcnow(self): + """ Test the UTC now method. """ + self.assertAlmostEqual( + dt_util.utcnow().replace(tzinfo=None), + datetime.utcnow(), + delta=timedelta(seconds=1)) + + def test_now(self): + """ Test the now method. """ + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) + + self.assertAlmostEqual( + dt_util.as_utc(dt_util.now()).replace(tzinfo=None), + datetime.utcnow(), + delta=timedelta(seconds=1)) + + def test_as_utc_with_naive_object(self): + utcnow = datetime.utcnow() + + self.assertEqual(utcnow, + dt_util.as_utc(utcnow).replace(tzinfo=None)) + + def test_as_utc_with_utc_object(self): + utcnow = dt_util.utcnow() + + self.assertEqual(utcnow, dt_util.as_utc(utcnow)) + + def test_as_utc_with_local_object(self): + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) + + localnow = dt_util.now() + + utcnow = dt_util.as_utc(localnow) + + self.assertEqual(localnow, utcnow) + self.assertNotEqual(localnow.tzinfo, utcnow.tzinfo) + + def test_as_local_with_naive_object(self): + now = dt_util.now() + + self.assertAlmostEqual( + now, dt_util.as_local(datetime.utcnow()), + delta=timedelta(seconds=1)) + + def test_as_local_with_local_object(self): + now = dt_util.now() + + self.assertEqual(now, now) + + def test_as_local_with_utc_object(self): + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) + + utcnow = dt_util.utcnow() + localnow = dt_util.as_local(utcnow) + + self.assertEqual(localnow, utcnow) + self.assertNotEqual(localnow.tzinfo, utcnow.tzinfo) + + def test_utc_from_timestamp(self): + """ Test utc_from_timestamp method. """ + self.assertEqual( + datetime(1986, 7, 9, tzinfo=dt_util.UTC), + dt_util.utc_from_timestamp(521251200)) + + def test_datetime_to_str(self): + """ Test datetime_to_str. """ + self.assertEqual( + "12:00:00 09-07-1986", + dt_util.datetime_to_str(datetime(1986, 7, 9, 12, 0, 0))) + + def test_datetime_to_local_str(self): + """ Test datetime_to_local_str. """ + self.assertEqual( + dt_util.datetime_to_str(dt_util.now()), + dt_util.datetime_to_local_str(dt_util.utcnow())) + + def test_str_to_datetime_converts_correctly(self): + """ Test str_to_datetime converts strings. """ + self.assertEqual( + datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC), + dt_util.str_to_datetime("12:00:00 09-07-1986")) + + def test_str_to_datetime_returns_none_for_incorrect_format(self): + """ Test str_to_datetime returns None if incorrect format. """ + self.assertIsNone(dt_util.str_to_datetime("not a datetime string")) + + def test_strip_microseconds(self): + test_time = datetime(2015, 1, 1, microsecond=5000) + + self.assertNotEqual(0, test_time.microsecond) + self.assertEqual(0, dt_util.strip_microseconds(test_time).microsecond)