Use UTC as the internal datetime format

pull/113/head
Paulus Schoutsen 2015-04-28 19:12:05 -07:00
parent 362e176397
commit e0ecb64a10
17 changed files with 457 additions and 147 deletions

View File

@ -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):
@ -472,13 +496,13 @@ 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 = 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 +534,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 +550,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 +572,7 @@ class State(object):
return "<state {}={}{} @ {}>".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 +609,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 +871,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():

View File

@ -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):

View File

@ -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 = []

View File

@ -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'
@ -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')

View File

@ -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().date()
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,

View File

@ -15,6 +15,7 @@ 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 +61,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 +74,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 +115,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 +168,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. """
@ -209,31 +212,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. """
@ -282,7 +287,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)
@ -341,6 +346,44 @@ class Recorder(threading.Thread):
save_migration(2)
if migration_id < 3:
utc_offset = self.utc_offset
cur.execute("""
ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE events
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE states
ADD COLUMN utc_offset integer
""")
cur.execute("UPDATE schema_version SET performed=performed+?",
(utc_offset,))
cur.execute("""
UPDATE recorder_runs SET utc_offset=?,
start=start + ?, end=end + ?, created=created + ?
""", [utc_offset]*4)
cur.execute("""
UPDATE events SET utc_offset=?,
time_fired=time_fired + ?, created=created + ?
""", [utc_offset]*3)
cur.execute("""
UPDATE states SET utc_offset=?, last_changed=last_changed + ?,
last_updated=last_updated + ?, created=created + ?
""", [utc_offset]*4)
save_migration(3)
def _close_connection(self):
""" Close connection to the database. """
_LOGGER.info("Closing database")
@ -357,18 +400,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 time.mktime(date_util.as_utc(datetimestamp).timetuple())
def _verify_instance():

View File

@ -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):

View File

@ -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):

View File

@ -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

99
homeassistant/util/dt.py Normal file
View File

@ -0,0 +1,99 @@
"""
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.utcnow().replace(tzinfo=pytz.utc)
def now(time_zone=None):
""" Get now in specified time zone. """
if time_zone is None:
time_zone = DEFAULT_TIME_ZONE
return utcnow().astimezone(time_zone)
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.fromtimestamp(timestamp, 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)

View File

@ -1,6 +1,7 @@
# required for Home Assistant core
requests>=2.0
pyyaml>=3.11
pytz>=2015.2
# optional, needed for specific components

View File

@ -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

View File

@ -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
@ -116,7 +117,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.

View File

@ -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()

View File

@ -72,7 +72,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 +88,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)

View File

@ -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'],

137
tests/test_util_dt.py Normal file
View File

@ -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)