Save and restore state of lights

Well, admittedly, I’m pretty new to HA, but from what I can understand, it’s not possible to tell the difference, especially from a python_script. (This is probably by design.) The only thing I can think of would require modifying light/group.py (either directly, or maybe better by copying it to your custom_components/light folder) to make it add another attribute (e.g., ‘light_group’, whose value can probably be anything but None.) But that’s probably not a good idea.

Your idea is probably better - i.e., pass a parameter into the scripts which is a list of lights to ignore (which would be the entity_id’s of the lights that are actually groups.) Of course, this would be less than optimal because you’d have to make sure to update this list every time you added or removed a light group. :frowning: Or possibly the list could be a list of lights to save/restore, but then again, maintaining the list would be problematic.

On the other hand, there might be occasions when you only want to save/restore a subset of all the switches & lights, so providing a list to save/restore might not be a bad feature. It could always default to all if no list was provided. And as I think I commented before (in the referenced topic), it might be nice to be able to specify a domain name for the “store” (instead of ‘light_store’) so that multiple, independent groups could be saved/restored. Hmm…

For the time being, either hardcoding entities to black- or whitelist them (whichever is shorter; in my case whitelisting would be more convenient) would work.

In the long run though, adding an attribute to differentiate between actual lights, and light groups only seems to be sensible. I’ve opened a feature request for it (issue 14081). (Light groups imported through Philips Hue already have an attribute is_hue_group: true!)

As for color, adding these lines to save_lights.py, before hass.states.set, should do the trick, I think;

rgb_color = cur_state.attributes.get('rgb_color')
        if rgb_color is not None:
            attributes['rgb_color'] = rgb_color

And the following to restore_lights.py, before hass.services.call:

rgb_color = old_state.attributes.get('rgb_color')
        if turn_on and rgb_color is not None:
            service_data['rgb_color'] = rgb_color 

So here’s an update that stores multiple attributes for lights (and makes it easy to add others. See below.)

FWIW, the light component seems to translate all color variants (xy_color, rgb_color, color_name) into hs_color (and back again when reporting attributes.) So I figured it was best to save/restore hs_color, but of course, feel free to modify the scripts if you feel otherwise.

I’ll work on adding support for a list of entitiy_id’s to save/restore next.

Updated save_lights.py:

DOMAIN = 'light_store'
STORE_ENTITY_ID = '{}.{{}}'.format(DOMAIN)

# Select light attributes to save/restore.
ATTR_BRIGHTNESS = "brightness"
ATTR_HS_COLOR = "hs_color"
LIGHT_ATTRS = [ATTR_BRIGHTNESS, ATTR_HS_COLOR]

def store_entity_id(entity_id):
    global STORE_ENTITY_ID
    return STORE_ENTITY_ID.format(entity_id.replace('.', '_'))

# Clear out any previously saved states.
saved = hass.states.entity_ids(DOMAIN)
for entity_id in saved:
    hass.states.remove(entity_id)

all_switches = hass.states.entity_ids('switch')
all_lights = hass.states.entity_ids('light')

for entity_id in all_switches:
    cur_state = hass.states.get(entity_id)
    if cur_state is None:
        logger.error('Could not get current state for {}.'.format(entity_id))
    else:
        hass.states.set(store_entity_id(entity_id), cur_state.state)

for entity_id in all_lights:
    cur_state = hass.states.get(entity_id)
    if cur_state is None:
        logger.error('Could not get current state for {}.'.format(entity_id))
    else:
        attributes = {}
        for attr in LIGHT_ATTRS:
            value = cur_state.attributes.get(attr)
            if value is not None:
                attributes[attr] = value
        hass.states.set(store_entity_id(entity_id), cur_state.state, attributes)

Updated restore_lights.py:

DOMAIN = 'light_store'
STORE_ENTITY_ID = '{}.{{}}'.format(DOMAIN)

# Select light attributes to save/restore.
ATTR_BRIGHTNESS = "brightness"
ATTR_HS_COLOR = "hs_color"
LIGHT_ATTRS = [ATTR_BRIGHTNESS, ATTR_HS_COLOR]

def store_entity_id(entity_id):
    global STORE_ENTITY_ID
    return STORE_ENTITY_ID.format(entity_id.replace('.', '_'))

# Retrieve saved states.
saved = hass.states.entity_ids(DOMAIN)

all_switches = hass.states.entity_ids('switch')
all_lights = hass.states.entity_ids('light')

for entity_id in all_switches:
    old_state = hass.states.get(store_entity_id(entity_id))
    if old_state is None:
        logger.error('No saved state for {}.'.format(entity_id))
    else:
        turn_on = old_state.state == 'on'
        service_data = {'entity_id': entity_id}
        hass.services.call('switch', 'turn_on' if turn_on else 'turn_off',
                           service_data)

for entity_id in all_lights:
    old_state = hass.states.get(store_entity_id(entity_id))
    if old_state is None:
        logger.error('No saved state for {}.'.format(entity_id))
    else:
        turn_on = old_state.state == 'on'
        service_data = {'entity_id': entity_id}
        if turn_on:
            for attr in LIGHT_ATTRS:
                value = old_state.attributes.get(attr)
                if value is not None:
                    service_data[attr] = value
        hass.services.call('light', 'turn_on' if turn_on else 'turn_off',
                           service_data)

# Remove saved states now that we're done with them.
for entity_id in saved:
    hass.states.remove(entity_id)

Ok, it took longer than expected. When they say, “The scripts are run in a sandboxed environment,” they’re not kidding!

In any case, you can now pass the following data to the scripts, both of which are optional:

store_name - Domain name used for switch/light store. Defaults to "light_store".
entity_id  - Entity IDs of switches and/or lights to save/restore.
             Defaults to all existing switches and lights.

Note that entity_id can be a single item, a list of items, or a comma separated string. E.g.:

"entity_id": "light.bulb1"
"entity_id": ["switch.switch1", "light.bulb1"]
"entity_id": "switch.switch1, light.bulb1"

Updated save_lights.py:

DOMAIN = 'light_store'

# Select light attributes to save.
ATTR_BRIGHTNESS = "brightness"
ATTR_HS_COLOR = "hs_color"
LIGHT_ATTRS = [ATTR_BRIGHTNESS, ATTR_HS_COLOR]

def store_entity_id(store_name, entity_id):
    return '{}.{}'.format(store_name, entity_id.replace('.', '_'))

# Get optional store name (default to DOMAIN.)
store_name = data.get('store_name', DOMAIN)

# Get optional list (or comma separated string) of switches & lights to
# save. If not provided, default to all.
entity_id = data.get('entity_id')
if isinstance(entity_id, str):
    entity_id = [e.strip() for e in entity_id.split(',')]

# Get lists of switches and lights to save that actually exist.
switches = hass.states.entity_ids('switch')
lights   = hass.states.entity_ids('light')
if entity_id:
    switches = tuple(set(entity_id).intersection(set(switches)))
    lights   = tuple(set(entity_id).intersection(set(lights)))

# Clear out any previously saved states.
saved = hass.states.entity_ids(store_name)
for entity_id in saved:
    hass.states.remove(entity_id)

for entity_id in switches:
    cur_state = hass.states.get(entity_id)
    if cur_state is None:
        logger.error('Could not get current state for {}.'.format(entity_id))
    else:
        hass.states.set(store_entity_id(store_name, entity_id), cur_state.state)

for entity_id in lights:
    cur_state = hass.states.get(entity_id)
    if cur_state is None:
        logger.error('Could not get current state for {}.'.format(entity_id))
    else:
        attributes = {}
        for attr in LIGHT_ATTRS:
            value = cur_state.attributes.get(attr)
            if value is not None:
                attributes[attr] = value
        hass.states.set(store_entity_id(store_name, entity_id), cur_state.state,
                        attributes)

Updated restore_lights.py:

DOMAIN = 'light_store'

# Select light attributes to restore.
ATTR_BRIGHTNESS = "brightness"
ATTR_HS_COLOR = "hs_color"
LIGHT_ATTRS = [ATTR_BRIGHTNESS, ATTR_HS_COLOR]

def store_entity_id(store_name, entity_id):
    return '{}.{}'.format(store_name, entity_id.replace('.', '_'))

# Get optional store name (default to DOMAIN.)
store_name = data.get('store_name', DOMAIN)

# Get optional list (or comma separated string) of switches & lights to
# restore. If not provided, default to all.
entity_id = data.get('entity_id')
if isinstance(entity_id, str):
    entity_id = [e.strip() for e in entity_id.split(',')]

# Get lists of switches and lights to restore that actually exist.
switches = hass.states.entity_ids('switch')
lights   = hass.states.entity_ids('light')
if entity_id:
    switches = tuple(set(entity_id).intersection(set(switches)))
    lights   = tuple(set(entity_id).intersection(set(lights)))

# Retrieve saved states.
saved = hass.states.entity_ids(store_name)

for entity_id in switches:
    old_state = hass.states.get(store_entity_id(store_name, entity_id))
    if old_state is None:
        logger.error('No saved state for {}.'.format(entity_id))
    else:
        turn_on = old_state.state == 'on'
        service_data = {'entity_id': entity_id}
        hass.services.call('switch', 'turn_on' if turn_on else 'turn_off',
                           service_data)

for entity_id in lights:
    old_state = hass.states.get(store_entity_id(store_name, entity_id))
    if old_state is None:
        logger.error('No saved state for {}.'.format(entity_id))
    else:
        turn_on = old_state.state == 'on'
        service_data = {'entity_id': entity_id}
        if turn_on:
            for attr in LIGHT_ATTRS:
                value = old_state.attributes.get(attr)
                if value is not None:
                    service_data[attr] = value
        hass.services.call('light', 'turn_on' if turn_on else 'turn_off',
                           service_data)

# Remove saved states now that we're done with them.
for entity_id in saved:
    hass.states.remove(entity_id)

Hope you find this useful!

1 Like

Just wanted to say that this python script is working great so far. I used to accomplish something similar but it took a whole bunch of scripts, binary sensors etc.

1 Like

FWIW, I’m thinking of combining them into just one script with an additional ‘save’ vs ‘restore’ param since there is so much overlap between them. Also, I think it would be good to change the default behavior of the restore operation when there is no list of entity_id’s. I.e., in that case, instead of trying to restore everything, just restore the same entities that were saved. This would make using the script easier - i.e., you’d only have to list the entities once (for saving, unless, of course, for some reason, you want to restore only a subset of the ones that were saved, which would still be possible by specifying that subset for the restore operation.) Thoughts?

I went ahead and made the changes I was talking about. Here is now the one script that does both (python_scripts/light_store.py):

DOMAIN = 'light_store'

ATTR_OPERATION  = 'operation'
ATTR_OP_SAVE    = 'save'
ATTR_OP_RESTORE = 'restore'

ATTR_STORE_NAME = 'store_name'
ATTR_ENTITY_ID  = 'entity_id'

# Select light attributes to save/restore.
ATTR_BRIGHTNESS = "brightness"
ATTR_HS_COLOR = "hs_color"
LIGHT_ATTRS = [ATTR_BRIGHTNESS, ATTR_HS_COLOR]

def store_entity_id(store_name, entity_id):
    return '{}.{}'.format(store_name, entity_id.replace('.', '_'))

# Get operation (default to save.)
operation = data.get(ATTR_OPERATION, ATTR_OP_SAVE)
if operation not in [ATTR_OP_SAVE, ATTR_OP_RESTORE]:
    logger.error('Invalid operation. Expected {} or {}, got: {}'.format(
        ATTR_OP_SAVE, ATTR_OP_RESTORE, operation))
else:
    # Get optional store name (default to DOMAIN.)
    store_name = data.get(ATTR_STORE_NAME, DOMAIN)

    # Get optional list (or comma separated string) of switches & lights to
    # save/restore.
    entity_id = data.get(ATTR_ENTITY_ID)
    if isinstance(entity_id, str):
        entity_id = [e.strip() for e in entity_id.split(',')]

    # Get lists of switches and lights that actually exist,
    # and list of entities that were previously saved.
    entity_ids = (hass.states.entity_ids('switch') +
                  hass.states.entity_ids('light'))
    saved      = hass.states.entity_ids(store_name)
    # When restoring, limit to existing entities that were saved.
    if operation == ATTR_OP_RESTORE:
        saved_entity_ids = []
        for e in entity_ids:
            if store_entity_id(store_name, e) in saved:
                saved_entity_ids.append(e)
        entity_ids = saved_entity_ids
    # If a list of entities was specified, further limit to just those.
    # Otherwise, save all existing switches and lights, or restore
    # all existing switches and lights that were previously saved.
    if entity_id:
        entity_ids = tuple(set(entity_ids).intersection(set(entity_id)))

    if operation == ATTR_OP_SAVE:
        # Clear out any previously saved states.
        for entity_id in saved:
            hass.states.remove(entity_id)

        # Save selected switches and lights to store.
        for entity_id in entity_ids:
            cur_state = hass.states.get(entity_id)
            if cur_state is None:
                logger.error('Could not get state of {}.'.format(entity_id))
            else:
                attributes = {}
                if entity_id.startswith('light.'):
                    for attr in LIGHT_ATTRS:
                        value = cur_state.attributes.get(attr)
                        if value is not None:
                            attributes[attr] = value
                hass.states.set(store_entity_id(store_name, entity_id),
                                cur_state.state, attributes)
    else:
        # Restore selected switches and lights from store.
        for entity_id in entity_ids:
            old_state = hass.states.get(store_entity_id(store_name, entity_id))
            if old_state is None:
                logger.error('No saved state for {}.'.format(entity_id))
            else:
                turn_on = old_state.state == 'on'
                service_data = {'entity_id': entity_id}
                component = entity_id.split('.')[0]
                if component == 'light' and turn_on:
                    for attr in LIGHT_ATTRS:
                        value = old_state.attributes.get(attr)
                        if value is not None:
                            service_data[attr] = value
                hass.services.call(component,
                                   'turn_on' if turn_on else 'turn_off',
                                   service_data)

        # Remove saved states now that we're done with them.
        for entity_id in saved:
            hass.states.remove(entity_id)

I’ve tested it via the services page, but I haven’t yet converted over my automations & scripts to use it. I’ll do that next and update if I find any issues. Also, by all means, if you try it and find issues, please let me know.

2 Likes

FYI, I created the following topic where I’ll maintain my script:

3 Likes

I think a lot of user are missing the point of the home automation (smart house)

that we want it to happen NOW

I have heaps of smarts in my house but a reboot / restart the house is dum until everything it get in line with each other to me thats a fact of my smart house eg drive down the drive garge door open lights come on , walk out of office lights turn off.

for me house take about 2hours to when all the smarts kick in . i can live with that
door have been open/closed heater on/off getting to the stage Im only restarting when I have add a new switch/ senser.

I Like HA as it is it just does my smarts

But in saying that do hate it on my test box yes its a pain in the … but Im look ahead not in the next hour

these are just my comments about a smart house

Can you please explain how to use the save and restore option?

This is my testing package

automation:
  - id: 'light_store'
    alias: "[Licht] STORE"
    trigger: 
      - platform: homeassistant
        event: start
    action:
      service: python_script.light_store

  - id: 'test_light_restore'
    alias: "[Test] Light Restore"
    trigger:
      - platform: state
        entity_id: light.arbeitszimmer_led_fenster
    action:
      - delay: 00:00:05
      - service: python_script.light_store

This will successfully store all lights statuses when (re)starting Home Assistant. However, I don’t know what to put at the bottom…

To test, I want to change the brightness for light.arbeitszimmer_led_fenster; then the automation should wait 5 seconds, and then restore the brightness to its previous value.

Did you see the doc page?

Actually I missed it and just copied the code from this thread. I’ll look into it now, thank you.

Oh, I didn’t pay close enough attention. I thought we were in this topic:

lol

For those still wanting to accomplish this without jumping through too many hoops.
You can make use of scenes to save the state beforehand and then reapply it when you want to revert back to the previous state.

- id: 'doorbell_pressed'
  alias: 'Doorbell Pressed'
  description: 'Flash lights light blue when someone presses the doorbell'
  trigger:
  - entity_id: binary_sensor.doorbell_button 
    from: 'off'
    platform: state
    to: 'on'
  condition: []
  action:
  - data:
      scene_id: doorbell_notification_revert
      snapshot_entities: 'light.living_room'
    service: scene.create
  - data:
      effect: Facebook
      entity_id: light.living_room
    service: light.turn_on
  - delay: 00:00:04
  - data:
      entity_id: scene.doorbell_notification_revert
    service: scene.turn_on
5 Likes

Thanks alot for this! I found that using the python script for saving the state no longer worked but this does, and without having to add a custom script!

1 Like

The caveat is that the scene made by scene.create is temporary. In other words, this technique won’t work if the goal is to restore lights on startup (like what Phil’s python_script does).

From the documentation:

Creating scenes on the fly

Create a new scene without having to configure it by calling the scene.create service. This scene will be discarded after reloading the configuration.

Otherwise, yes, scene.create is a very useful way to store/restore the state of not only lights but other entities as well.


EDIT
I was wrong. The python_script has no advantage on restart (see pnbruckner’s comments below). Therefore your suggestion to use scene.create is functionally equivalent (and self-contained within one script).

Um, sorry to disappoint, but no, it doesn’t. It saves the info to the State Machine, not in any persistent storage. The info in the State Machine is volatile, and will be lost on a restart. (The info that appears to be saved across restarts is actually reloaded from persistent storage which is used purposely to save/restore, potentially partial, state info for entities.)

I think the dynamic scene capability has mostly made my script obsolete, although there are some things it still doesn’t do that my script does.

Pardon my thick skull but I don’t know which one you’re referring to, scene.create or the python_script.

Are you saying that the scene created by scene.create survives a restart?

Sorry, I meant to say that my python_script does not restore on startup, or more accurately, does not save to persistent storage, so cannot be used to restore state that was saved before a restart.

So I guess that means neither survives a restart.

I see. OK, then I misunderstood and attributed more functionality to the python_script than warranted. I’ll amend my previous post.