Restore states through a JSON store instead of recorder (#17270)

* Restore states through a JSON store

* Accept entity_id directly in restore state helper

* Keep states stored between runs for a limited time

* Remove warning
This commit is contained in:
Adam Mills
2018-11-28 07:16:43 -05:00
committed by Paulus Schoutsen
parent a039c3209b
commit 5c3a4e3d10
46 changed files with 493 additions and 422 deletions

View File

@@ -1,60 +1,52 @@
"""The tests for the Restore component."""
import asyncio
from datetime import timedelta
from unittest.mock import patch, MagicMock
from datetime import datetime
from homeassistant.setup import setup_component
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import CoreState, split_entity_id, State
import homeassistant.util.dt as dt_util
from homeassistant.components import input_boolean, recorder
from homeassistant.core import CoreState, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.restore_state import (
async_get_last_state, DATA_RESTORE_CACHE)
from homeassistant.components.recorder.models import RecorderRuns, States
RestoreStateData, RestoreEntity, DATA_RESTORE_STATE_TASK)
from homeassistant.util import dt as dt_util
from tests.common import (
get_test_home_assistant, mock_coro, init_recorder_component,
mock_component)
from asynctest import patch
from tests.common import mock_coro
@asyncio.coroutine
def test_caching_data(hass):
async def test_caching_data(hass):
"""Test that we cache data."""
mock_component(hass, 'recorder')
hass.state = CoreState.starting
states = [
State('input_boolean.b0', 'on'),
State('input_boolean.b1', 'on'),
State('input_boolean.b2', 'on'),
]
with patch('homeassistant.helpers.restore_state.last_recorder_run',
return_value=MagicMock(end=dt_util.utcnow())), \
patch('homeassistant.helpers.restore_state.get_states',
return_value=states), \
patch('homeassistant.helpers.restore_state.wait_connection_ready',
return_value=mock_coro(True)):
state = yield from async_get_last_state(hass, 'input_boolean.b1')
data = await RestoreStateData.async_get_instance(hass)
await data.store.async_save([state.as_dict() for state in states])
assert DATA_RESTORE_CACHE in hass.data
assert hass.data[DATA_RESTORE_CACHE] == {st.entity_id: st for st in states}
# Emulate a fresh load
hass.data[DATA_RESTORE_STATE_TASK] = None
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = 'input_boolean.b1'
# Mock that only b1 is present this run
with patch('homeassistant.helpers.restore_state.Store.async_save'
) as mock_write_data:
state = await entity.async_get_last_state()
assert state is not None
assert state.entity_id == 'input_boolean.b1'
assert state.state == 'on'
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
assert DATA_RESTORE_CACHE not in hass.data
assert mock_write_data.called
@asyncio.coroutine
def test_hass_running(hass):
"""Test that cache cannot be accessed while hass is running."""
mock_component(hass, 'recorder')
async def test_hass_starting(hass):
"""Test that we cache data."""
hass.state = CoreState.starting
states = [
State('input_boolean.b0', 'on'),
@@ -62,129 +54,144 @@ def test_hass_running(hass):
State('input_boolean.b2', 'on'),
]
with patch('homeassistant.helpers.restore_state.last_recorder_run',
return_value=MagicMock(end=dt_util.utcnow())), \
patch('homeassistant.helpers.restore_state.get_states',
return_value=states), \
patch('homeassistant.helpers.restore_state.wait_connection_ready',
return_value=mock_coro(True)):
state = yield from async_get_last_state(hass, 'input_boolean.b1')
assert state is None
data = await RestoreStateData.async_get_instance(hass)
await data.store.async_save([state.as_dict() for state in states])
# Emulate a fresh load
hass.data[DATA_RESTORE_STATE_TASK] = None
@asyncio.coroutine
def test_not_connected(hass):
"""Test that cache cannot be accessed if db connection times out."""
mock_component(hass, 'recorder')
hass.state = CoreState.starting
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = 'input_boolean.b1'
states = [State('input_boolean.b1', 'on')]
# Mock that only b1 is present this run
states = [
State('input_boolean.b1', 'on'),
]
with patch('homeassistant.helpers.restore_state.Store.async_save'
) as mock_write_data, patch.object(
hass.states, 'async_all', return_value=states):
state = await entity.async_get_last_state()
with patch('homeassistant.helpers.restore_state.last_recorder_run',
return_value=MagicMock(end=dt_util.utcnow())), \
patch('homeassistant.helpers.restore_state.get_states',
return_value=states), \
patch('homeassistant.helpers.restore_state.wait_connection_ready',
return_value=mock_coro(False)):
state = yield from async_get_last_state(hass, 'input_boolean.b1')
assert state is None
@asyncio.coroutine
def test_no_last_run_found(hass):
"""Test that cache cannot be accessed if no last run found."""
mock_component(hass, 'recorder')
hass.state = CoreState.starting
states = [State('input_boolean.b1', 'on')]
with patch('homeassistant.helpers.restore_state.last_recorder_run',
return_value=None), \
patch('homeassistant.helpers.restore_state.get_states',
return_value=states), \
patch('homeassistant.helpers.restore_state.wait_connection_ready',
return_value=mock_coro(True)):
state = yield from async_get_last_state(hass, 'input_boolean.b1')
assert state is None
@asyncio.coroutine
def test_cache_timeout(hass):
"""Test that cache timeout returns none."""
mock_component(hass, 'recorder')
hass.state = CoreState.starting
states = [State('input_boolean.b1', 'on')]
@asyncio.coroutine
def timeout_coro():
raise asyncio.TimeoutError()
with patch('homeassistant.helpers.restore_state.last_recorder_run',
return_value=MagicMock(end=dt_util.utcnow())), \
patch('homeassistant.helpers.restore_state.get_states',
return_value=states), \
patch('homeassistant.helpers.restore_state.wait_connection_ready',
return_value=timeout_coro()):
state = yield from async_get_last_state(hass, 'input_boolean.b1')
assert state is None
def _add_data_in_last_run(hass, entities):
"""Add test data in the last recorder_run."""
# pylint: disable=protected-access
t_now = dt_util.utcnow() - timedelta(minutes=10)
t_min_1 = t_now - timedelta(minutes=20)
t_min_2 = t_now - timedelta(minutes=30)
with recorder.session_scope(hass=hass) as session:
session.add(RecorderRuns(
start=t_min_2,
end=t_now,
created=t_min_2
))
for entity_id, state in entities.items():
session.add(States(
entity_id=entity_id,
domain=split_entity_id(entity_id)[0],
state=state,
attributes='{}',
last_changed=t_min_1,
last_updated=t_min_1,
created=t_min_1))
def test_filling_the_cache():
"""Test filling the cache from the DB."""
test_entity_id1 = 'input_boolean.b1'
test_entity_id2 = 'input_boolean.b2'
hass = get_test_home_assistant()
hass.state = CoreState.starting
init_recorder_component(hass)
_add_data_in_last_run(hass, {
test_entity_id1: 'on',
test_entity_id2: 'off',
})
hass.block_till_done()
setup_component(hass, input_boolean.DOMAIN, {
input_boolean.DOMAIN: {
'b1': None,
'b2': None,
}})
hass.start()
state = hass.states.get('input_boolean.b1')
assert state
assert state is not None
assert state.entity_id == 'input_boolean.b1'
assert state.state == 'on'
state = hass.states.get('input_boolean.b2')
assert state
assert state.state == 'off'
# Assert that no data was written yet, since hass is still starting.
assert not mock_write_data.called
hass.stop()
# Finish hass startup
with patch('homeassistant.helpers.restore_state.Store.async_save'
) as mock_write_data:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
# Assert that this session states were written
assert mock_write_data.called
async def test_dump_data(hass):
"""Test that we cache data."""
states = [
State('input_boolean.b0', 'on'),
State('input_boolean.b1', 'on'),
State('input_boolean.b2', 'on'),
]
entity = Entity()
entity.hass = hass
entity.entity_id = 'input_boolean.b0'
await entity.async_added_to_hass()
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = 'input_boolean.b1'
await entity.async_added_to_hass()
data = await RestoreStateData.async_get_instance(hass)
data.last_states = {
'input_boolean.b0': State('input_boolean.b0', 'off'),
'input_boolean.b1': State('input_boolean.b1', 'off'),
'input_boolean.b2': State('input_boolean.b2', 'off'),
'input_boolean.b3': State('input_boolean.b3', 'off'),
'input_boolean.b4': State(
'input_boolean.b4', 'off', last_updated=datetime(
1985, 10, 26, 1, 22, tzinfo=dt_util.UTC)),
}
with patch('homeassistant.helpers.restore_state.Store.async_save'
) as mock_write_data, patch.object(
hass.states, 'async_all', return_value=states):
await data.async_dump_states()
assert mock_write_data.called
args = mock_write_data.mock_calls[0][1]
written_states = args[0]
# b0 should not be written, since it didn't extend RestoreEntity
# b1 should be written, since it is present in the current run
# b2 should not be written, since it is not registered with the helper
# b3 should be written, since it is still not expired
# b4 should not be written, since it is now expired
assert len(written_states) == 2
assert written_states[0]['entity_id'] == 'input_boolean.b1'
assert written_states[0]['state'] == 'on'
assert written_states[1]['entity_id'] == 'input_boolean.b3'
assert written_states[1]['state'] == 'off'
# Test that removed entities are not persisted
await entity.async_will_remove_from_hass()
with patch('homeassistant.helpers.restore_state.Store.async_save'
) as mock_write_data, patch.object(
hass.states, 'async_all', return_value=states):
await data.async_dump_states()
assert mock_write_data.called
args = mock_write_data.mock_calls[0][1]
written_states = args[0]
assert len(written_states) == 1
assert written_states[0]['entity_id'] == 'input_boolean.b3'
assert written_states[0]['state'] == 'off'
async def test_dump_error(hass):
"""Test that we cache data."""
states = [
State('input_boolean.b0', 'on'),
State('input_boolean.b1', 'on'),
State('input_boolean.b2', 'on'),
]
entity = Entity()
entity.hass = hass
entity.entity_id = 'input_boolean.b0'
await entity.async_added_to_hass()
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = 'input_boolean.b1'
await entity.async_added_to_hass()
data = await RestoreStateData.async_get_instance(hass)
with patch('homeassistant.helpers.restore_state.Store.async_save',
return_value=mock_coro(exception=HomeAssistantError)
) as mock_write_data, patch.object(
hass.states, 'async_all', return_value=states):
await data.async_dump_states()
assert mock_write_data.called
async def test_load_error(hass):
"""Test that we cache data."""
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = 'input_boolean.b1'
with patch('homeassistant.helpers.storage.Store.async_load',
return_value=mock_coro(exception=HomeAssistantError)):
state = await entity.async_get_last_state()
assert state is None