implement get_significant_states

This adds a new function to history module which returns significant
states. For most domains this is the list of state changes. For the
thermostat domain this also includes attribute changes, so that
changes in the current_temperature are exposed to the graphing layer.

Closes #881
This commit is contained in:
Sean Dague 2016-01-23 15:36:43 -05:00
parent 3d00735341
commit abc253c4c5
2 changed files with 120 additions and 16 deletions

View file

@ -18,6 +18,8 @@ from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
SIGNIFICANT_DOMAINS = ('thermostat',)
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
@ -35,6 +37,37 @@ def last_5_states(entity_id):
return recorder.query_states(query, (entity_id, ))
def get_significant_states(start_time, end_time=None, entity_id=None):
"""Return states changes during UTC period start_time - end_time.
Significant states are all states where there is a state change,
as well as all states from certain domains (for instance
thermostat so that we get current temperature in our graphs).
"""
where = """
(domain in ({}) or last_changed=last_updated)
AND last_updated > ?
""".format(",".join(["'%s'" % x for x in SIGNIFICANT_DOMAINS]))
data = [start_time]
if end_time is not None:
where += "AND last_updated < ? "
data.append(end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_updated ASC").format(where)
states = recorder.query_states(query, data)
return states_to_json(states, start_time, entity_id)
def state_changes_during_period(start_time, end_time=None, entity_id=None):
"""
Return states changes during UTC period start_time - end_time.
@ -55,20 +88,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
states = recorder.query_states(query, data)
result = defaultdict(list)
entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time
for state in get_states(start_time, entity_ids):
state.last_changed = start_time
result[state.entity_id].append(state)
# Append all changes to it
for entity_id, group in groupby(states, lambda state: state.entity_id):
result[entity_id].extend(group)
return result
return states_to_json(states, start_time, entity_id)
def get_states(utc_point_in_time, entity_ids=None, run=None):
@ -100,6 +120,33 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
return recorder.query_states(query, where_data)
def states_to_json(states, start_time, entity_id):
"""Converts SQL results into JSON friendly data structure.
This takes our state list and turns it into a JSON friendly data
structure {'entity_id': [list of states], 'entity_id2': [list of states]}
We also need to go back and create a synthetic zero data point for
each list of states, otherwise our graphs won't start on the Y
axis correctly.
"""
result = defaultdict(list)
entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time
for state in get_states(start_time, entity_ids):
state.last_changed = start_time
state.last_updated = start_time
result[state.entity_id].append(state)
# Append all changes to it
for entity_id, group in groupby(states, lambda state: state.entity_id):
result[entity_id].extend(group)
return result
def get_state(utc_point_in_time, entity_id, run=None):
""" Return a state at a specific point in time. """
states = get_states(utc_point_in_time, (entity_id,), run)
@ -152,4 +199,4 @@ def _api_history_period(handler, path_match, data):
entity_id = data.get('filter_entity_id')
handler.write_json(
state_changes_during_period(start_time, end_time, entity_id).values())
get_significant_states(start_time, end_time, entity_id).values())

View file

@ -8,7 +8,7 @@ Tests the history component.
from datetime import timedelta
import os
import unittest
from unittest.mock import patch
from unittest.mock import patch, sentinel
import homeassistant.core as ha
import homeassistant.util.dt as dt_util
@ -143,3 +143,60 @@ class TestComponentHistory(unittest.TestCase):
hist = history.state_changes_during_period(start, end, entity_id)
self.assertEqual(states, hist[entity_id])
def test_get_significant_states(self):
"""test that only significant states are returned with
get_significant_states.
We inject a bunch of state updates from media player and
thermostat. We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
self.init_recorder()
mp = 'media_player.test'
therm = 'thermostat.test'
def set_state(entity_id, state, **kwargs):
self.hass.states.set(entity_id, state, **kwargs)
self.wait_recording_done()
return self.hass.states.get(entity_id)
zero = dt_util.utcnow()
one = zero + timedelta(seconds=1)
two = one + timedelta(seconds=1)
three = two + timedelta(seconds=1)
four = three + timedelta(seconds=1)
states = {therm: [], mp: []}
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=one):
states[mp].append(
set_state(mp, 'idle',
attributes={'media_title': str(sentinel.mt1)}))
states[mp].append(
set_state(mp, 'YouTube',
attributes={'media_title': str(sentinel.mt2)}))
states[therm].append(
set_state(therm, 20, attributes={'current_temperature': 19.5}))
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=two):
# this state will be skipped only different in time
set_state(mp, 'YouTube',
attributes={'media_title': str(sentinel.mt3)})
states[therm].append(
set_state(therm, 21, attributes={'current_temperature': 19.8}))
with patch('homeassistant.components.recorder.dt_util.utcnow',
return_value=three):
states[mp].append(
set_state(mp, 'Netflix',
attributes={'media_title': str(sentinel.mt4)}))
# attributes changed even though state is the same
states[therm].append(
set_state(therm, 21, attributes={'current_temperature': 20}))
hist = history.get_significant_states(zero, four)
self.assertEqual(states, hist)