8000 Add history_graph component by andrey-git · Pull Request #9472 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add history_graph component #9472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions homeassistant/components/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def last_recorder_run(hass):
return res


def get_significant_states(hass, start_time, end_time=None, entity_id=None,
filters=None):
def get_significant_states(hass, start_time, end_time=None, entity_ids=None,
filters=None, include_start_time_state=True):
"""
Return states changes during UTC period start_time - end_time.

Expand All @@ -60,8 +60,6 @@ def get_significant_states(hass, start_time, end_time=None, entity_id=None,
timer_start = time.perf_counter()
from homeassistant.components.recorder.models import States

entity_ids = (entity_id.lower(), ) if entity_id is not None else None

with session_scope(hass=hass) as session:
query = session.query(States).filter(
(States.domain.in_(SIGNIFICANT_DOMAINS) |
Expand All @@ -86,7 +84,9 @@ def get_significant_states(hass, start_time, end_time=None, entity_id=None,
_LOGGER.debug(
'get_significant_states took %fs', elapsed)

return states_to_json(hass, states, start_time, entity_id, filters)
return states_to_json(
hass, states, start_time, entity_ids, filters,
include_start_time_state)


def state_changes_during_period(hass, start_time, end_time=None,
Expand All @@ -105,10 +105,12 @@ def state_changes_during_period(hass, start_time, end_time=None,
if entity_id is not None:
query = query.filter_by(entity_id=entity_id.lower())

entity_ids = [entity_id] if entity_id is not None else None

states = execute(
query.order_by(States.last_updated))

return states_to_json(hass, states, start_time, entity_id)
return states_to_json(hass, states, start_time, entity_ids)


def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
Expand Down Expand Up @@ -185,7 +187,13 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
if not state.attributes.get(ATTR_HIDDEN, False)]


def states_to_json(hass, states, start_time, entity_id, filters=None):
def states_to_json(
hass,
states,
start_time,
entity_ids,
filters=None,
include_start_time_state=True):
"""Convert SQL results into JSON friendly data structure.

This takes our state list and turns it into a JSON friendly data
Expand All @@ -197,14 +205,13 @@ def states_to_json(hass, states, start_time, entity_id, filters=None):
"""
result = defaultdict(list)

entity_ids = [entity_id] if entity_id is not None else None

# Get the states at the start time
timer_start = time.perf_counter()
for state in get_states(hass, start_time, entity_ids, filters=filters):
state.last_changed = start_time
state.last_updated = start_time
result[state.entity_id].append(state)
if include_start_time_state:
for state in get_states(hass, start_time, entity_ids, filters=filters):
state.last_changed = start_time
state.last_updated = start_time
result[state.entity_id].append(state)

if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start
Expand Down Expand Up @@ -250,7 +257,7 @@ class HistoryPeriodView(HomeAssistantView):
extra_urls = ['/api/history/period/{datetime}']

def __init__(self, filters):
"""Initilalize the history period view."""
"""Initialize the history period view."""
self.filters = filters

@asyncio.coroutine
Expand Down Expand Up @@ -282,11 +289,14 @@ def get(self, request, datetime=None):
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
else:
end_time = start_time + one_day
entity_id = request.query.get('filter_entity_id')
entity_ids = request.query.get('filter_entity_id')
if entity_ids:
entity_ids = entity_ids.lower().split(',')
include_start_time_state = 'skip_initial_state' not in request.query

result = yield from request.app['hass'].async_add_job(
get_significant_states, request.app['hass'], start_time, end_time,
entity_id, self.filters)
entity_ids, self.filters, include_start_time_state)
result = result.values()
if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start
Expand Down
87 changes: 87 additions & 0 deletions homeassistant/components/history_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Support to graphs card in the UI.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history_graph/
"""
import asyncio
import logging

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_ENTITIES, CONF_NAME, ATTR_ENTITY_ID
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent

DEPENDENCIES = ['history']

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'history_graph'

CONF_HOURS_TO_SHOW = 'hours_to_show'
CONF_REFRESH = 'refresh'
ATTR_HOURS_TO_SHOW = CONF_HOURS_TO_SHOW
ATTR_REFRESH = CONF_REFRESH


GRAPH_SCHEMA = vol.Schema({
vol.Required(CONF_ENTITIES): cv.entity_ids,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOURS_TO_SHOW, default=24): vol.Range(min=1),
vol.Optional(CONF_REFRESH, default=0): vol.Range(min=0),
})


CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({cv.slug: GRAPH_SCHEMA})
}, extra=vol.ALLOW_EXTRA)


@asyncio.coroutine
def async_setup(hass, config):
"""Load graph configurations."""
component = EntityComponent(
_LOGGER, DOMAIN, hass)
graphs = []

for object_id, cfg in config[DOMAIN].items():
name = cfg.get(CONF_NAME, object_id)
graph = HistoryGraphEntity(name, cfg)
graphs.append(graph)

yield from component.async_add_entities(graphs)

return True


class HistoryGraphEntity(Entity):
"""Representation of a graph entity."""

def __init__(self, name, cfg):
"""Initialize the graph."""
self._name = name
self._hours = cfg[CONF_HOURS_TO_SHOW]
self._refresh = cfg[CONF_REFRESH]
self._entities = cfg[CONF_ENTITIES]

@property
def should_poll(self):
"""No polling needed."""
return False

@property
def name(self):
"""Return the name of the entity."""
return self._name

@property
def state_attributes(self):
"""Return the state attributes."""
attrs = {
ATTR_HOURS_TO_SHOW: self._hours,
ATTR_REFRESH: self._refresh,
ATTR_ENTITY_ID: self._entities,
}
return attrs
56 changes: 55 additions & 1 deletion tests/components/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,48 @@ def test_get_significant_states(self):
self.hass, zero, four, filters=history.Filters())
assert states == hist

def test_get_significant_states_with_initial(self):
"""Test that only significant states are returned.

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).
"""
zero, four, states = self.record_states()
+ timedelta(seconds=1)
one_and_half = zero + timedelta(seconds=1.5)
for entity_id in states:
if entity_id == 'media_player.test':
states[entity_id] = states[entity_id][1:]
for state in states[entity_id]:
if state.last_changed == one:
state.last_changed = one_and_half

hist = history.get_significant_states(
self.hass, one_and_half, four, filters=history.Filters(),
include_start_time_state=True)
assert states == hist

def test_get_significant_states_without_initial(self):
"""Test that only significant states are returned.

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).
"""
zero, four, states = self.record_states()
+ timedelta(seconds=1)
one_and_half = zero + timedelta(seconds=1.5)
for entity_id in states:
states[entity_id] = list(filter(
lambda s: s.last_changed != one, states[entity_id]))
del states['media_player.test2']

hist = history.get_significant_states(
self.hass, one_and_half, four, filters=history.Filters(),
include_start_time_state=False)
assert states == hist

def test_get_significant_states_entity_id(self):
"""Test that only significant states are returned for one entity."""
zero, four, states = self.record_states()
Expand All @@ -154,7 +196,19 @@ def test_get_significant_states_entity_id(self):
del states['script.can_cancel_this_one']

hist = history.get_significant_states(
self.hass, zero, four, 'media_player.test',
self.hass, zero, four, ['media_player.test'],
filters=history.Filters())
assert states == hist

def test_get_significant_states_multiple_entity_ids(self):
"""Test that only significant states are returned for one entity."""
zero, four, states = self.record_states()
del states['media_player.test2']
del states['thermostat.test2']
del states['script.can_cancel_this_one']

hist = history.get_significant_states(
self.hass, zero, four, ['media_player.test', 'thermostat.test'],
filters=history.Filters())
assert states == hist

Expand Down
46 changes: 46 additions & 0 deletions tests/components/test_history_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""The tests the Graph component."""

import unittest

from homeassistant.setup import setup_component
from tests.common import init_recorder_component, get_test_home_assistant


class TestGraph(unittest.TestCase):
"""Test the Google component."""

def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()

def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()

def test_setup_component(self):
"""Test setup component."""
self.init_recorder()
config = {
'history': {
},
'history_graph': {
'name_1': {
'entities': 'test.test',
}
}
}

self.assertTrue(setup_component(self.hass, 'history_graph', config))
self.assertEqual(
dict(self.hass.states.get('history_graph.name_1').attributes),
{
'entity_id': ['test.test'],
'friendly_name': 'name_1',
'hours_to_show': 24,
'refresh': 0
})

def init_recorder(self):
"""Initialize the recorder."""
init_recorder_component(self.hass)
self.hass.start()
0