-
Notifications
You must be signed in to change notification settings - Fork 42
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
Persist pyscript.* states between home assistant restarts #48
Persist pyscript.* states between home assistant restarts #48
Conversation
With this change any states set by pyscript in the pyscript domain will be persisted when home assistant next reboots. For example when a script runs `pyscript.abc = 'foo'` then this value will be persisted. Fixes custom-components#47
8daa4b8
to
1865eca
Compare
I tested this with the following code:
There is an issue. If I set a value and then restart, that value is there. However, if I then restart again (without setting it again) that value goes away. Also, we likely need a way to REMOVE a set value. |
According to the home assistant code comments states should be persisted
until some kind of expiry date as long as they were previously persisted.
Which is how i figured to solve both issues, but that clearly isn't working
then. I'll look into it, thanks for testing.
…On Sat, Oct 17, 2020, 12:40 Daniel Lashua ***@***.***> wrote:
I tested this with the follow code:
@service
def testset(**params):
try:
cnt = int(pyscript.testsetcnt)
except:
pyscript.testsetcnt = 0
cnt = 0
if "name" not in params:
log.error("name required")
return
if "value" not in params:
log.error("value required")
return
attributes = params.get("attributes", {})
if not isinstance(attributes, dict):
log.error("attributes must be a dict")
return
state.set(f'pyscript.{params["name"]}', params["value"], attributes)
cnt = cnt + 1
pyscript.testsetcnt = cnt
There is an issue.
If I set a value and then restart, that value is there. However, if I then
restart again (without setting it again) that value goes away.
Also, we likely need a way to REMOVE a set value.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#48 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/APVGX2EH475WKNAU5E7FAUTSLFYC7ANCNFSM4SUFX4MA>
.
|
# have this var tracked for restore | ||
restore_data = await RestoreStateData.async_get_instance(cls.hass) | ||
restore_data.async_restore_entity_added(var_name) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How expensive is it to call these functions every time a pyscript state variable is set? Would it be better to only do it the first time each state variable is set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very experienced with the Home Assistant codebase or python async code.
The async_restore_entity_added
simply puts the entry on a set
, so it isn't expensive at all. The get_instance method I'm not sure, I expect it to be cheap due to the singleton? https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/restore_state.py#L65
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've since introduced our own set to track persisted states, so this code block will only evaluate once.
The state expiration built into There are two scenarios to consider for deleting state variables:
For scenario 1 I am considering the following solutions:
I'm leaning towards solution 3, as that would still allow succinct scripts for simple cases. So I'll work on updating the PR accordingly. However, do please let me know if you have a different preference or additional thoughts. Thanks! |
Automatically persist pyscript.* state variables that are read from pyscript, either directly or using a state_trigger. Also provide a state.persist method to pyscript to explicitly register variables to be persisted.
The example provided by @dlashua will still not automatically work as-is with my latest commit, as the state variable names are entirely dynamic. The only way to support this out of the box is to permanently persist all This example explicitly registers the state variable, which is static, and works as expected: state.persist("pyscript.count_hours", default_value=0)
@time_trigger("period(0:00, 1 hour)")
def hourly():
pyscript.count_hours = int(pyscript.count_hours) + 1 This example doesn't explicitly register the state variable, but does have a state_trigger, so it also works as expected: @state_trigger("True or pyscript.some_var")
def some_var_changed():
log.error(f"some_var: {pyscript.some_var}")
@service
def set_some_var(value):
pyscript.some_var = value However, the following example DOES NOT work as expected! The state variable is set conditionally, and never read or explicitly registered (because it is dynamic): @service
def set_var(key, value):
# This var will be lost after restart!
state.set(f"pyscript.{key}", value) A user could work around this by using a special prefix, though: for var_name in state.names("pyscript"):
if var_name.startswith("pyscript.set_var_"):
log.error(f"Explicitly persisting: {var_name}")
state.persist(var_name)
# or: [state.persist(var) for var in state.names("pyscript") if var.startswith("pyscript.set_var_")]
@service
def set_var(key, value):
state.set(f"pyscript.set_var_{key}", value) Users could also just choose to permanently persist all variables by dropping the It might also be possible to simplify the final example by supplying a Thoughts? |
I wound up implementing So now the following can be done: state.persist("pyscript.some_var", default_value=123)
state.persist_prefix("pyscript.set_var_")
@service
def set_var(key, value):
state.set(f"pyscript.set_var_{key}", value) |
It is now also possible to do the following: state.persist("pyscript.light", "on", {
'brightness': 255,
'color': [255, 255, 255]
}) If the state already existed and a new attribute is added to the |
To make sure I understand what you have here... In order to be sure a variable persists, I will need to do one of the following at some point after EACH Home Assistant start:
In addition to the above requirements, only entities beginning with In practice, any entity I want persisted should be explicitly persisted using There is no need to "delete" persisted entities. Simply removing the pyscript code using that entity and any calls to For additional clarity, using the entity in a Home Assistant Automation, Template Sensor/Binary Sensor, or Script will not ensure its persistence. One of the 4 methods listed above must be used in pyscript code. If this is correct, I think the above text (or something like it) should be included in the Documentation. We should also consider REQUIRING the use of |
@dlashua That is all largely correct.
Or mention the entity in a
I've decided to limit the scope of all of this to
Yes. Or having a
Correct.
Correct.
I agree that the documentation should encourage users to always call Disabling the automatic persisting on read/write will make the implementation of
So if we were to completely disable the automatic persisting on read/write, then this would have to be solved otherwise. Possibly by checking the set of states that have been passed to |
If having it in |
This is my thinking too.
Thanks! I was hoping for further review from @craigbarratt before writing docs that describe these new features, but I've given you write access to my fork so if you feel so inclined, have at it 😄 |
Thanks for the progress on this. I still don't understand the use cases well enough to know what the best solution is. Fixed "persistent" parameters would be done via config. So I presume it's mainly internal states (of anything) that you want to persist so a restart is not so disruptive. First, stepping back a little, HASS state variables aren't a great way to preserve useful information since they are coerced into strings, although their attributes can be arbitrary types (I presume they at least need to be serializable). On the plus side, HASS already supports persistence of state variables, so that saves us from having to develop and maintain some other mechanism. Since the HASS state variable namespace is global, there is also the issue of different pyscript apps colliding if they fail to use unique names. Combining these pros/cons, I somewhat agree that persisting HASS state variables is the right choice, although the namespace collision risks are a concern. The alternative of persisting designated global variables in each app's context would avoid the namespace collision, and allow richer data types to be persistent (so long as they are serializable), but then we'd have to come up with a new way to do persistence, which is highly undesirable. I do worry about the performance also, if every persistent variable setting causes a disk write (at least eventually). I'm not sure how HASS does that - are they cached and flushed only periodically? Presumably it turns into a database write, so it should be relatively efficient. I'd prefer not to have several different explicit and implicit mechanisms (eg, get and set) that denote persistence. I think it would be cleaner if there was just one way to do it. For example, Alternatively, we could have a pyscript.my_important_var.persist = True Reactions? |
My intention is to create HASS states that can be used in other automations, while also having them be persisted like Namespace collisions are a risk, but that is a risk that already exists. Any pyscript app that exists today can already set arbitrary states and collide with other pyscript apps (or other custom components or anything, really). Adding persistence to these states doesn't change anything about that. Some apps seem to solve this by letting the user configure the state to use, where the user is expected to pre-create a I want to make it as easy as possible for me, as a developer writing automations for my own home, to declare new persisted state variables that I can use to drive my home. That means I would really prefer to be able to do this all from within pyscript, without having to go through other config files for each state parameter I wish to store. Especially when those state parameters may depend upon auto-discovery of other entities, and as such cannot be predicated ahead of time easily. I do agree that combining implicit and explicit registration of persisted variables is probably going to be confusing, though the current implementation does the right thing for most use-cases. My preferred approach to requiring explicit persistence would be to keep the This does mean a new set will have to be introduced of all prefixes that get passed to Alternatively we could drop for var_name in state.names("pyscript"):
if var_name.startswith("pyscript.set_var_"):
state.persist(var_name)
@service
def set_var(key, value):
state.persist("pyscript.set_var_{key}")
state.set(f"pyscript.set_var_{key}", value) I'd probably wind up having to include such a snippet in every pyscript I make, then. So I'd like a solution that keeps the capability of
This shouldn't be that much of a concern, the states are collected and flushed periodically (15 minutes) and upon HASS shutdown. A lot of components are also already utilizing this state storage by using the So with all that said I propose the following: # This is required for any state variable that should be persisted. Best practice is to put this on the top-level in a pyscript. If this method is not executed during the pyscript lifecycle the persisted value will eventually be lost.
state.persist("pyscript.my_var", default_value="on", default_attributes={'some_attr': 123})
# Alternatively it is possible to register all states with a certain prefix to be persisted
state.persist_prefix("pyscript.some_prefix_") The The @classmethod
async def persist_prefix(cls, prefix):
"""Ensures all pyscript domain state variable with prefix are persisted."""
if prefix.count(".") != 1 or not prefix.startswith("pyscript."):
raise NameError(f"invalid prefix {prefix} (should be 'pyscript.entity')")
# Store the prefix to ensure the prefixed var is also persisted if it doesn't yet exist
cls.persisted_prefixes.add(prefix)
for name in await cls.names("pyscript"):
if name.startswith(prefix):
await cls.register_persist(name) And the @classmethod
async def set(cls, var_name, value=None, new_attributes=None, **kwargs):
"""Set a state variable and optional attributes in hass."""
# ...
if var_name.startswith("pyscript.") and var_name not in cls.persisted_vars and any(var_name.startswith(prefix) for prefix in cls.persisted_prefixes):
await cls.register_persist(var_name)
cls.hass.states.async_set(var_name, value, new_attributes) And the changes to |
I wanted to add that I don't think it would be desirable to allow pyscript to let states outside of the I also am not a big fan of the |
There are several situations where persisting state is useful. I'm sure this list isn't comprehensive.
I'm sure I missed something as I'm just making this up in the comment window, but I think you get the idea. There are ways around this. Primarily, we use As @swazrgb indicated, namespace collisions are a risk. But it's somewhat in the nature of Home Assistant. These days, when there's a collision on, say The "right" way to do this is to register each entity using all the entity registry stuff that I've never bothered to unravel the code for. Home Assistant will, then, try to assign an entity_id according to the "name" of the entity, but will name it whatever it wants if it can't. Then the user can change the entity_id in the UI. But, doing this makes it harder to access that entity in other pyscripts because it may have been named differently than you expect. |
I also use this functionality to create derived states, which can be based on other state changes & events. Due to them being based on events some kind of persistence is necessary. An example is the hue dimmer switch, this sends an event for each button press. I'd like to store the dim or scene state in a pyscript state variable (or attribute thereof), which ideally has persistence. Without persistence all the lights reset when HA restarts. |
add documentation
Thanks for the really excellent comments and background. It's really helpful to understand potential use cases. Ok, my preference is to keep things as simple as possible, so let's go with just: # This is required for any state variable that should be persisted. Best practice is to put this on the top-level in a pyscript. If this method is not executed during the pyscript lifecycle the persisted value will eventually be lost.
state.persist("pyscript.my_var", default_value="on", default_attributes={'some_attr': 123}) Scripts that want persistence would have a sequence of calls like this, with the default values being applied only the first time. And that should be all that you need - there shouldn't be any need to call While I do hear your points about the benefits of It sounds like we should add support for using Thanks again for your patience - this looks like a very useful feature. |
I have updated this branch to include only state.persist(). I have corrected the documentation. I have tested with the following script:
|
docs/reference.rst
Outdated
@@ -551,12 +551,11 @@ variable has a numeric value, you might want to convert it to a numeric type (eg | |||
Persistent State | |||
^^^^^^^^^^^^^^^^ | |||
|
|||
Two methods are provided to explicitly indicate that a particular entity_id should be persisted. | |||
This method is provided to indicate that a particular entity_id should be persisted. This is only effective for entitys in the `pyscript` domain. | |||
|
|||
``state.persist(entity_id, default_value=None)`` | |||
Indicates that the entity named in `entity_id` should be persisted. Optionally, a default value can be provided. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add optional default_attributes=None
argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
With this change any states set by pyscript in the pyscript domain
will be persisted when home assistant next reboots.
For example when a script runs
pyscript.abc = 'foo'
then this valuewill be persisted.
Fixes #47