Save and restore state of lights

Here’s two. One that currently is composed of lights currently all switched off; another composed of some lights currently switched on (but not all). Neither of these is a light.entity, but a light.group (eg. light.roomb and light.rooma).

First consists of two actual lights (bulb1 and 2), whereas the second is composed of three. None of these details are exposed in attributes. Perhaps filtering out “supported_features=63” might do the trick; don’t think I’ve seen it in light entities (eg. light.bulbs).

No; found a light.bulb with that attribute.

I’m going to take a look at the light/init.py and light/group.py code tonight. There may be a way to do this. I’ll try to get back to you tomorrow.

As far as I can see, there is no way to tell the difference. This is why I’m thinking a white-/blacklist of entity_id:s under a domain might be the only way to make it work?

Great! Much appreciated. (The color-thing I might take a stab at myself tomorrow; seems straight forward enough to add.)

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