Skip to content

Restore_state helper to restore entity states from the DB on startup#4614

Merged
balloob merged 8 commits into
home-assistant:devfrom
kellerza:restore
Feb 21, 2017
Merged

Restore_state helper to restore entity states from the DB on startup#4614
balloob merged 8 commits into
home-assistant:devfrom
kellerza:restore

Conversation

@kellerza
Copy link
Copy Markdown
Member

@kellerza kellerza commented Nov 28, 2016

Description:
Add a helper function to restore previous entity states from the database/recorder when Entities are added to HASS.

Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>

Example entry for configuration.yaml (if applicable):

recorder:  # Recorder would be required

Checklist:

If user exposed functionality or configuration variables are added/changed:

If the code does not interact with devices:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

@mention-bot
Copy link
Copy Markdown

@kellerza, thanks for your PR! By analyzing the history of the files in this pull request, we identified @nkgilley, @rhooper and @balloob to be potential reviewers.

@balloob
Copy link
Copy Markdown
Member

balloob commented Nov 28, 2016

Some quick comments:

  • this should not be part of the recorder but a standalone component
  • on start, it should not overwrite a state if it already exists
  • an extra attribute has to be added to indicate that it is restored
  • we should update the UI to allow to dismiss restored entities
  • History has a method to get all states at a point in time. Use that with last session end time - 1 instead of adding your own method.
  • Maybe restore all entities by default at end of last run unless whitelist/blacklist used

@arsaboo
Copy link
Copy Markdown
Contributor

arsaboo commented Nov 29, 2016

Wondering if this can be expanded to support persistence in general. For example, I use input_boolean.alokhome (that obtains information from multiple device trackers) to track presence. I am guessing (or hoping) that the following config will restore input_boolean.alokhome to the last known state.

recorder:
    restore: input_boolean.alokhome

@kellerza
Copy link
Copy Markdown
Member Author

@arsaboo Yes, in it's current for that should work for persistence. I simply used the device_tracker as an example, but also have input_selects and input_booleans that would benefit from this.

What should we call this component? restore or persistent/persistence
We already have persistent_notifications, so maybe restore?

@arsaboo
Copy link
Copy Markdown
Contributor

arsaboo commented Nov 30, 2016

@kellerza restore sounds good 👍

@balloob
Copy link
Copy Markdown
Member

balloob commented Nov 30, 2016

Let's be a bit more specific to what it restores. What about restore_entity ?

@emilhetty
Copy link
Copy Markdown
Contributor

I would love to have the climate target temperature to be restored from DB as well.
If I change the target temperature in the UI now and restart HA it returns to target temperature given in the config file.

@balloob balloob self-assigned this Dec 2, 2016
@kellerza kellerza changed the title Restore states from the DB on startup WIP: Restore_entity component to restore states from the DB on startup Dec 3, 2016
@kellerza
Copy link
Copy Markdown
Member Author

kellerza commented Dec 3, 2016

To be continued... (after 18 Dec)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just call .first()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this method should live in history so that history can be the API on top of the recorder ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

history makes sense, was just concerned about it also requiring http, but it will make things cleaner

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to query it again? get_states will already return the state at that point in time.

You should however make sure that the state not already exists. We don't want to overwrite legitimate states. We should also make sure that you add some attribute to indicate that this is a restored state, otherwise we won't be able to show a dismiss button in the UI.

cur_state = hass.states.get(entity_id)

if cur_state is not None:
    continue

attributes = dict(state.attributes)
attributes[ATTR_RESTORED_STATE] = True

hass.states.set(entity_id, state.state, attributes)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will add the attribute. Actually I have not managed to get past line 60 (with the comment), so this second loading is still an artefact from the previous recorder version.

Once I get my unit test's test data to be returned by get_states I'll continue with this part. Had some challenges to mock recorder.models.dayetime.utcnow Since we never import models directly and datetime also a challenge

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples of not mocking time of models but cant find the DEBUG output from line 60 in Travis. I do see some SQLAlchemy exceptions, which wasnt so clear on my machine.... obviously still needs some work to add state entries before HASS start and ensure recorder starts up correctly during HASS start

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides being unnecessary (see my other comment), this method is pretty much covered by this method

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference is this method ignores unknown states, but reusing the old also cleaner since the possibility of unknown in the DB should be less toward the end of a session

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep any integration into other components out of the initial release. Let's really focus on the core support.

Because if we do it right, components/platforms don't have to restore anything. They can just not set a state and then restoration will do it instead .

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

@kellerza
Copy link
Copy Markdown
Member Author

Seems like I created a flaky test here. Py34 passing, py35 not...

Seem that my HOME_ASSISTANT_START event is not always fired. Any suggestions would be appreciated

@balloob
Copy link
Copy Markdown
Member

balloob commented Jan 17, 2017

What's the statu of this @kellerza ?

@yawor
Copy link
Copy Markdown

yawor commented Jan 19, 2017

I really look forward to use this component. As for the name I think restore_states or state_restore would be more appropriate as it restores states of the entities, not the entities themselves :).

@kellerza
Copy link
Copy Markdown
Member Author

Will look into this again. Was struggling with temperamental unit tests and will give it another shot this weekend.

Will squash all commits on rebase, since there are some conflicts.

I like restore_states!

@mannkind
Copy link
Copy Markdown

Just adding some positive support for this component. I'm really looking forward to the usability increase this will bring to HA.

@kellerza
Copy link
Copy Markdown
Member Author

kellerza commented Jan 21, 2017

Latest update:

  • Removed all conflicts (squashed all previous versions)
  • Now named restore_state
  • Still getting some flaky test runs locally

Will try to spend more time on these flaky tests tomorrow

@dinki
Copy link
Copy Markdown

dinki commented Jan 23, 2017

Excited about this as well. I'll ask a dumb question as to if I can try this now? What does it take to add this to an existing install?

@kellerza
Copy link
Copy Markdown
Member Author

kellerza commented Jan 24, 2017

At the moment this works well for sensor

For input_* and switch a state always exists on startup, which of these two approaches will be best:

  • Overwrite the state
  • Force the value by calling the service of the entity - see the MAP in the latest commit
  • import the methods directly into the MAP structure instead of duplicating the service schemas in MAP switch.turn_on, switch.turn_off, input_*.toggle, etc

I expect for device_tracker we will need to call see

EDIT: Added option 3, which is better for linting etc, but create lots of dependencies for restore_state

@kellerza
Copy link
Copy Markdown
Member Author

@dinki the best would be to clone the dev branch in a new directory - see here

@balloob
Copy link
Copy Markdown
Member

balloob commented Feb 14, 2017

If the recorder connection won't be initialized until Home Assistant starts, how will we be able to query for the states?

@kellerza
Copy link
Copy Markdown
Member Author

The recorder init changed so that we can query states before start - #4614 (comment)

vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line missing indentation or outdented

vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line unaligned for hanging indent

vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line unaligned for hanging indent

Copy link
Copy Markdown
Member

@balloob balloob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So close! I had a few minor comments but the overall structure is awesome.

rec_runs = recorder.get_model('RecorderRuns')
with recorder.session_scope() as session:
res = recorder.query(rec_runs).order_by(rec_runs.end.desc()).first()
session.expunge(res)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first() can return None which will blow up when passed to session.expunge

Comment thread homeassistant/components/history.py Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postponed for future PR

"in %s seconds)", err, CONNECT_RETRY_WAIT)
time.sleep(CONNECT_RETRY_WAIT)

load_restore_cache(self.hass)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this here and instead just initialize it the first time get_last_state is called. Cache should only be initialized if hass.state == CoreState.starting (CoreState can be imported from homeassistant.core)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be done, I've got a new unit test (should be non-flakey) where I discovered I'll have to yield get_last_state in any case. On startup many components will potentially wait for this event, which hopefully won't affect order of adding to hass. At least by the time this is called "entities" was already updated in entity_component

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI For now, we set up all components and platforms in sequence.

def get_last_state(entity: Entity, check_async_restore_state: bool=True):
"""Helper to restore state."""
recorder.get_instance()
if check_async_restore_state and \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this check from this method. It is not a concern from the recorder what this is being used for.

def async_added_to_hass(self):
"""Component added, restore_state using platforms."""
state = get_last_state(self)
if state:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you convert this to a guard clause? This example is going to be copied by a lot of other component/platform developers and thus needs to be perfect 🥇

if state is None:
    return

params =

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your view on moving this to homeassistant.helpers.restore_state?

Two reasons:

  • recorder does not explicitly have to request the cache, but get_last_state handles this.
  • Many components & platforms (possibly every sensor) will import this "helper"

The cache function can then be a _private

Copy link
Copy Markdown
Member

@balloob balloob Feb 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add another helper to this file that wraps the entity logic around get_state.

def extract_info(state):
    """Restore properties from state object."""
    return {
        is_on: state.state == STATE_ON,
        # etc…
    }

class Light:
    def async_added_to_hass(self):
        yield from restore_state.xxxxx(self, extract_info)

And in restore state, the helper xxxxx does all the logic that was in light before.

def xxxxx(entity, extract_info):
    if not hasattr(entity, 'async_restore_state'):
        return

    state = get_state(entity.entity_id)

    if not state:
        return

    yield from entity.async_restore_state(**extract_info(state))

@asyncio.coroutine
def async_added_to_hass(self):
"""Component added, restore_state using platforms."""
state = get_last_state(self)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_last_state should only take in an entity_id.

if not hasattr(self, 'async_restore_state'):
    return

state = get_last_state(self.entity_id)

@kellerza kellerza changed the title WIP: Restore_state component to restore states from the DB on startup Restore_state helper to restore entity states from the DB on startup Feb 20, 2017
Copy link
Copy Markdown
Member

@balloob balloob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wooooohooooooooooo 🎉

One minor signature change required. You can merge after the comment has been addressed 🐬

Comment thread homeassistant/helpers/restore_state.py Outdated


@asyncio.coroutine
def async_get_last_state(entity: Entity):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameters should be async_get_last_state(hass, entity_id)

Comment thread homeassistant/helpers/restore_state.py Outdated
if not hasattr(entity, 'async_restore_state'):
return

state = yield from async_get_last_state(entity.entity_id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should pass in hass too.

Interestingly. Here we pass entity_id to the async_get_last_state method but it expects an entity. This didn't get caught by the tests.


@asyncio.coroutine
def async_added_to_hass(self):
"""Component added to hass, no platforms, so restore state here."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be async_get_last_state(self.hass, self.entity_id)

@balloob
Copy link
Copy Markdown
Member

balloob commented Feb 21, 2017

Fixed my own comments and added some tests

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.