Skip to content

Commit 827a1fc

Browse files
authored
Merge pull request #48 from swazrgb/persist-state
Persist pyscript.* states between home assistant restarts
2 parents 0c1e929 + 656665b commit 827a1fc

File tree

5 files changed

+106
-8
lines changed

5 files changed

+106
-8
lines changed

custom_components/pyscript/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from homeassistant.exceptions import HomeAssistantError
1919
import homeassistant.helpers.config_validation as cv
20+
from homeassistant.helpers.restore_state import RestoreStateData
2021
from homeassistant.loader import bind_hass
2122

2223
from .const import (
@@ -50,6 +51,7 @@
5051

5152
async def async_setup(hass, config):
5253
"""Component setup, run import config flow for each entry in config."""
54+
await restore_state(hass)
5355
if DOMAIN in config:
5456
hass.async_create_task(
5557
hass.config_entries.flow.async_init(
@@ -60,6 +62,15 @@ async def async_setup(hass, config):
6062
return True
6163

6264

65+
async def restore_state(hass):
66+
"""Restores the persisted pyscript state."""
67+
restore_data = await RestoreStateData.async_get_instance(hass)
68+
for entity_id, value in restore_data.last_states.items():
69+
if entity_id.startswith("pyscript."):
70+
last_state = value.state
71+
hass.states.async_set(entity_id, last_state.state, last_state.attributes)
72+
73+
6374
async def async_setup_entry(hass, config_entry):
6475
"""Initialize the pyscript config entry."""
6576
Function.init(hass)

custom_components/pyscript/eval.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,7 @@ async def recurse_assign(self, lhs, val):
11681168
if not isinstance(var_name, str):
11691169
raise NotImplementedError(f"unknown lhs type {lhs} (got {var_name}) in assign")
11701170
if var_name.find(".") >= 0:
1171-
State.set(var_name, val)
1171+
await State.set(var_name, val)
11721172
return
11731173
if self.curr_func and var_name in self.curr_func.global_names:
11741174
self.global_sym_table[var_name] = val
@@ -1307,7 +1307,7 @@ async def ast_name(self, arg):
13071307
# a two-dot name for state.attr needs to exist
13081308
#
13091309
if num_dots == 1 or (num_dots == 2 and State.exist(arg.id)):
1310-
return State.get(arg.id)
1310+
return await State.get(arg.id)
13111311
#
13121312
# Couldn't find it, so return just the name wrapped in EvalName to
13131313
# distinguish from a string variable value. This is to support

custom_components/pyscript/state.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import logging
44

5+
from homeassistant.helpers.restore_state import RestoreStateData
6+
57
from .const import LOGGER_PATH
68
from .function import Function
79

@@ -34,6 +36,11 @@ class State:
3436
#
3537
pyscript_config = {}
3638

39+
#
40+
# pyscript vars which have already been registered as persisted
41+
#
42+
persisted_vars = set()
43+
3744
def __init__(self):
3845
"""Warn on State instantiation."""
3946
_LOGGER.error("State class is not meant to be instantiated")
@@ -44,7 +51,7 @@ def init(cls, hass):
4451
cls.hass = hass
4552

4653
@classmethod
47-
def notify_add(cls, var_names, queue):
54+
async def notify_add(cls, var_names, queue):
4855
"""Register to notify state variables changes to be sent to queue."""
4956

5057
for var_name in var_names if isinstance(var_names, set) else {var_names}:
@@ -98,7 +105,7 @@ def notify_var_get(cls, var_names, new_vars):
98105
return notify_vars
99106

100107
@classmethod
101-
def set(cls, var_name, value, new_attributes=None, **kwargs):
108+
async def set(cls, var_name, value, new_attributes=None, **kwargs):
102109
"""Set a state variable and optional attributes in hass."""
103110
if var_name.count(".") != 1:
104111
raise NameError(f"invalid name {var_name} (should be 'domain.entity')")
@@ -113,8 +120,34 @@ def set(cls, var_name, value, new_attributes=None, **kwargs):
113120
new_attributes.update(kwargs)
114121
_LOGGER.debug("setting %s = %s, attr = %s", var_name, value, new_attributes)
115122
cls.notify_var_last[var_name] = str(value)
123+
116124
cls.hass.states.async_set(var_name, value, new_attributes)
117125

126+
@classmethod
127+
async def register_persist(cls, var_name):
128+
"""Persists a pyscript state variable using RestoreState."""
129+
if var_name.startswith("pyscript.") and var_name not in cls.persisted_vars:
130+
restore_data = await RestoreStateData.async_get_instance(cls.hass)
131+
restore_data.async_restore_entity_added(var_name)
132+
cls.persisted_vars.add(var_name)
133+
134+
@classmethod
135+
async def persist(cls, var_name, default_value=None, default_attributes=None):
136+
"""Ensures a pyscript domain state variable is persisted."""
137+
if var_name.count(".") != 1 or not var_name.startswith("pyscript."):
138+
raise NameError(f"invalid name {var_name} (should be 'pyscript.entity')")
139+
140+
await cls.register_persist(var_name)
141+
exists = cls.exist(var_name)
142+
143+
if not exists and default_value is not None:
144+
await cls.set(var_name, default_value, default_attributes)
145+
elif exists and default_attributes is not None:
146+
# Patch the attributes with new values if necessary
147+
current = cls.hass.states.get(var_name)
148+
new_attributes = {k: v for (k, v) in default_attributes.items() if k not in current.attributes}
149+
await cls.set(var_name, current.state, **new_attributes)
150+
118151
@classmethod
119152
def exist(cls, var_name):
120153
"""Check if a state variable value or attribute exists in hass."""
@@ -125,7 +158,7 @@ def exist(cls, var_name):
125158
return value and (len(parts) == 2 or parts[2] in value.attributes)
126159

127160
@classmethod
128-
def get(cls, var_name):
161+
async def get(cls, var_name):
129162
"""Get a state variable value or attribute from hass."""
130163
parts = var_name.split(".")
131164
if len(parts) != 2 and len(parts) != 3:
@@ -140,7 +173,7 @@ def get(cls, var_name):
140173
return value.attributes.get(parts[2])
141174

142175
@classmethod
143-
def get_attr(cls, var_name):
176+
async def get_attr(cls, var_name):
144177
"""Return a dict of attributes for a state variable."""
145178
if var_name.count(".") != 1:
146179
raise NameError(f"invalid name {var_name} (should be 'domain.entity')")
@@ -188,6 +221,7 @@ def register_functions(cls):
188221
"state.set": cls.set,
189222
"state.names": cls.names,
190223
"state.get_attr": cls.get_attr,
224+
"state.persist": cls.persist,
191225
"pyscript.config": cls.pyscript_config,
192226
}
193227
Function.register(functions)

custom_components/pyscript/trigger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async def wait_until(
159159
"trigger %s wait_until: watching vars %s", ast_ctx.name, state_trig_ident,
160160
)
161161
if len(state_trig_ident) > 0:
162-
State.notify_add(state_trig_ident, notify_q)
162+
await State.notify_add(state_trig_ident, notify_q)
163163
if event_trigger is not None:
164164
if isinstance(event_trigger, str):
165165
event_trigger = [event_trigger]
@@ -595,7 +595,7 @@ async def trigger_watch(self):
595595
self.state_trig_ident.update(self.state_trig_ident_any)
596596
_LOGGER.debug("trigger %s: watching vars %s", self.name, self.state_trig_ident)
597597
if len(self.state_trig_ident) > 0:
598-
State.notify_add(self.state_trig_ident, self.notify_q)
598+
await State.notify_add(self.state_trig_ident, self.notify_q)
599599

600600
if self.active_expr:
601601
self.state_active_ident = await self.active_expr.get_names()

docs/reference.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,16 @@ Note that in HASS, all state variable values are coerced into strings. For examp
564564
variable has a numeric value, you might want to convert it to a numeric type (eg, using ``int()`` or
565565
``float()``). Attributes keep their native type.
566566

567+
Persistent State
568+
^^^^^^^^^^^^^^^^
569+
570+
This method is provided to indicate that a particular entity_id should be persisted. This is only effective for entitys in the `pyscript` domain.
571+
572+
``state.persist(entity_id, default_value=None, default_attributes=None)``
573+
Indicates that the entity named in `entity_id` should be persisted. Optionally, a default value and default attributes can be provided.
574+
575+
576+
567577
Service Calls
568578
^^^^^^^^^^^^^
569579

@@ -1204,3 +1214,46 @@ is optional in pyscript):
12041214
async with session.get(url) as resp:
12051215
print(resp.status)
12061216
print(resp.text())
1217+
1218+
Persistent State
1219+
^^^^^^^^^^^^^^^^
1220+
1221+
Pyscript has the ability to persist state in the `pyscript.` domain. This means that setting an entity like `pyscript.test` will cause it to be restored to its previous state when Home Assistant is restarted.
1222+
1223+
This can be done in any of the usual ways to set the state of an `entity_id`:
1224+
1225+
.. code:: python
1226+
1227+
set.state('pyscript.test', 'on')
1228+
1229+
pyscript.test = 'on'
1230+
1231+
Attributes can be included:
1232+
1233+
.. code:: python
1234+
1235+
set.state('pyscript.test', 'on', friendly_name="Test", device_class="motion")
1236+
1237+
pyscript.test = 'on'
1238+
pyscript.test.friendly_name = 'Test'
1239+
pyscript.test.device_class = 'motion'
1240+
1241+
In order to ensure that the state of a particular entity persists, you need to request persistence explicitly. This must be done in a code location that will be certain to run at startup. Generally, this means outside of trigger functions.
1242+
1243+
1244+
.. code:: python
1245+
1246+
state.persist('pyscript.last_light_on')
1247+
1248+
@state_trigger('binary_sensor.motion == "on"')
1249+
def turn_on_lights():
1250+
light.turn_on('light.overhead')
1251+
pyscript.last_light_on = "light.overhead"
1252+
1253+
With this in place, `state.persist()` will be called every time this script is parsed, ensuring this particular state will persist.
1254+
1255+
1256+
1257+
1258+
1259+

0 commit comments

Comments
 (0)