mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 18:39:30 +00:00
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:
committed by
Paulus Schoutsen
parent
a039c3209b
commit
5c3a4e3d10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user