Python_script to save and restore switches and lights

Hello! Thanks for this script.

I did a little modifications to it, to allow to save the state of a light and restore it to a different one.

I’m not sure how to create a git diff, but maybe a unified one will suffice:

--- light_store.py.1	2020-03-30 02:19:42.000000000 -0300
+++ copy_state.py	2020-03-30 01:53:52.000000000 -0300
@@ -9,6 +9,7 @@

 ATTR_STORE_NAME = 'store_name'
 ATTR_ENTITY_ID  = 'entity_id'
+ATTR_ALT_ENTITY_ID  = 'alt_entity_id'

 # Select light attributes to save/restore.
 ATTR_BRIGHTNESS = "brightness"
@@ -42,6 +43,8 @@
     if isinstance(entity_id, str):
         entity_id = [e.strip() for e in entity_id.split(',')]

+    alt_entity_id = data.get(ATTR_ALT_ENTITY_ID)
+
     # Replace any group entities with their contents.
     # Repeat until no groups left in list.
     expanded_a_group = True
@@ -106,6 +109,8 @@
                 logger.error('No saved state for {}.'.format(entity_id))
             else:
                 turn_on = old_state.state == 'on'
+                if alt_entity_id is not None:
+                    entity_id = alt_entity_id
                 service_data = {'entity_id': entity_id}
                 component = entity_id.split('.')[0]
                 if component == 'light' and turn_on and old_state.attributes:
@@ -117,3 +122,4 @@
         # Remove saved states now that we're done with them.
         for entity_id in saved:
             hass.states.remove(entity_id)
+

And I’m using it this way:

light_copy_state:
  alias: copy a light state
  sequence:

  - service: python_script.copy_state
    data:
      store_name: flash_store
      entity_id: light.hue_1

  - service: python_script.copy_state
    data:
      store_name: flash_store
      entity_id: light.hue_1
      alt_entity_id: light.hue_2
      operation: restore

The “alt_entity_id” will get the state of the “entity_id”.

I’m not sure if this will work on anything different from a single light, but that’s all I need right now.

Thanks for your original script again :slight_smile:

Juan

Some time ago I have created a custom component that allows saving and using variables and states. It might be useful for you :slight_smile:

I got a problem with saving effects. Spefically, my case:

  1. My led strip is off, I save it’s state.
  2. I turn them on with some effect for a while.
  3. I restore lights.

The result? The lights are off, but they have the effect stored. So next time I turn on these ligths (without any parameter), they play effect that was selected in point 2).

Not desirable…

It’s important to point out, that it works exactly as the built in dynamic scenes, but still, it’s kinda buggy.

Since I updated to Home Assistant version 2023.11.1 I noticed that the script no longer works.
Can someone help me with my problem?

Logger: homeassistant.components.automation.good_night_house
Source: components/automation/__init__.py:655
Integration: Automatisierung (documentation, issues)
First occurred: 18:36:10 (1 occurrences)
Last logged: 18:36:10

While executing automation automation.good_night_house
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/automation/__init__.py", line 655, in async_trigger
    await self.action_script.async_run(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 1578, in async_run
    return await asyncio.shield(run.async_run())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 420, in async_run
    await self._async_step(log_exceptions=False)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 470, in _async_step
    self._handle_exception(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 493, in _handle_exception
    raise exception
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 468, in _async_step
    await getattr(self, handler)()
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 704, in _async_call_service_step
    response_data = await self._async_run_long_action(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 666, in _async_run_long_action
    return long_task.result()
           ^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2035, in async_call
    response_data = await coro
                    ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2072, in _execute_service
    return await target(service_call)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 235, in handle_service
    return await service.entity_service_call(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 904, in entity_service_call
    task.result()  # pop exception if have
    ^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1219, in async_request_call
    return await coro
           ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 948, in _handle_entity_call
    result = await task
             ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/light/__init__.py", line 591, in async_handle_light_off_service
    await light.async_turn_off(**filter_turn_off_params(light, params))
  File "/usr/src/homeassistant/homeassistant/components/wled/helpers.py", line 28, in handler
    await func(self, *args, **kwargs)
  File "/usr/src/homeassistant/homeassistant/components/wled/light.py", line 208, in async_turn_off
    await self.coordinator.wled.segment(
  File "/usr/local/lib/python3.11/site-packages/wled/wled.py", line 515, in segment
    await self.request("/json/state", method="POST", data=state)
  File "/usr/local/lib/python3.11/site-packages/backoff/_async.py", line 151, in retry
    ret = await target(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/wled/wled.py", line 211, in request
    response_data = await response.json()
                    ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/aiohttp_client.py", line 84, in json
    return await super().json(*args, loads=loads, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/client_reqrep.py", line 1120, in json
    return loads(stripped.decode(encoding))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
orjson.JSONDecodeError: unexpected character: line 1 column 862 (char 861)

Logger: homeassistant.components.automation.good_night_house
Source: helpers/script.py:468
Integration: Automatisierung (documentation, issues)
First occurred: 18:36:10 (1 occurrences)
Last logged: 18:36:10

Good Night House: Error executing script. Unexpected error for call_service at pos 3: unexpected character: line 1 column 862 (char 861)
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 468, in _async_step
    await getattr(self, handler)()
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 704, in _async_call_service_step
    response_data = await self._async_run_long_action(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 666, in _async_run_long_action
    return long_task.result()
           ^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2035, in async_call
    response_data = await coro
                    ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2072, in _execute_service
    return await target(service_call)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 235, in handle_service
    return await service.entity_service_call(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 904, in entity_service_call
    task.result()  # pop exception if have
    ^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1219, in async_request_call
    return await coro
           ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 948, in _handle_entity_call
    result = await task
             ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/light/__init__.py", line 591, in async_handle_light_off_service
    await light.async_turn_off(**filter_turn_off_params(light, params))
  File "/usr/src/homeassistant/homeassistant/components/wled/helpers.py", line 28, in handler
    await func(self, *args, **kwargs)
  File "/usr/src/homeassistant/homeassistant/components/wled/light.py", line 208, in async_turn_off
    await self.coordinator.wled.segment(
  File "/usr/local/lib/python3.11/site-packages/wled/wled.py", line 515, in segment
    await self.request("/json/state", method="POST", data=state)
  File "/usr/local/lib/python3.11/site-packages/backoff/_async.py", line 151, in retry
    ret = await target(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/wled/wled.py", line 211, in request
    response_data = await response.json()
                    ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/aiohttp_client.py", line 84, in json
    return await super().json(*args, loads=loads, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/client_reqrep.py", line 1120, in json
    return loads(stripped.decode(encoding))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
orjson.JSONDecodeError: unexpected character: line 1 column 862 (char 861)

automation:

##############################################################################################################################
### Restore Lights ### switch groups or other stuff ### Verwendung in automation.good_night_house
######################
restore_lights_on:
  alias: Turn on a given selection of lights, saving current state
  sequence:
    - service: python_script.light_store
      data:
        store_name: flash_store
        entity_id:
          - light.bett_leds
          - light.lampe_rgb_schlafzimmer
          - light.schlafzimmer_deckenlampe
          - light.lena_stehlampe
          - light.sonoff_mini_lena
          - light.tobias_zimmerlicht
          - light.einfahrt_decken_lampe
          - light.einfahrtlicht
          - light.opel_licht
          - light.einfahrt_decken_lampe_fake
          - switch.kasten
          - light.wled_badezimmer
          - light.badezimmer_ikea_lampe
          - light.badezimmer_deckenlampe
          - light.nono_led
          - light.og
          - light.ttgo_neopixel_light
          - light.matrix
    - service: homeassistant.turn_on

restore_lights:
  alias: Restore saved lights to the way they were
  sequence:
    - service: python_script.light_store
      data:
        store_name: flash_store
        operation: restore

The error looks like it happens during the automation named Good Night House, but you shared two scripts. Could you share the automation?

@pnbruckner I am using 1.2.0 version and started to have in HA 2023.11 (it was working ok in 2023.10):

Logger: homeassistant.components.python_script.light_store.py
Source: components/python_script/__init__.py:224
Integration: Python Scripts (documentation, issues)
First occurred: 8 listopada 2023 17:37:23 (15 occurrences)
Last logged: 19:18:34

Error executing script: expected int for dictionary value @ data['color_temp']
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/python_script/__init__.py", line 224, in execute
    exec(compiled.code, restricted_globals)  # noqa: S102
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "light_store.py", line 113, in <module>
  File "/usr/src/homeassistant/homeassistant/core.py", line 1940, in call
    ).result()
      ^^^^^^^^
  File "/usr/local/lib/python3.11/concurrent/futures/_base.py", line 456, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/usr/src/homeassistant/homeassistant/core.py", line 2001, in async_call
    processed_data: dict[str, Any] = handler.schema(service_data)
                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 232, in __call__
    return self._exec((Schema(val) for val in self.validators), v)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 355, in _exec
    raise e if self.msg is None else AllInvalid(self.msg, path=path)
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 351, in _exec
    v = func(v)
        ^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 272, in __call__
    return self._compiled([], data)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 818, in validate_callable
    return schema(data)
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 272, in __call__
    return self._compiled([], data)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 229, in _run
    return self._exec(self._compiled, value, path)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 355, in _exec
    raise e if self.msg is None else AllInvalid(self.msg, path=path)
  File "/usr/local/lib/python3.11/site-packages/voluptuous/validators.py", line 353, in _exec
    v = func(path, v)
        ^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 818, in validate_callable
    return schema(data)
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 272, in __call__
    return self._compiled([], data)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 595, in validate_dict
    return base_validate(path, iteritems(data), out)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/voluptuous/schema_builder.py", line 433, in validate_mapping
    raise er.MultipleInvalid(errors)
voluptuous.error.MultipleInvalid: expected int for dictionary value @ data['color_temp']

Does 1.3.0 fixes that?

I believe it should. It has to do with a change in 2023.11 that now includes attributes whose value is None. It didn’t use to do that. 1.3.0 is meant to handle that change.

1 Like

i use the new version!

VERSION = '1.3.0'

DOMAIN = 'light_store'

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

ATTR_STORE_NAME = 'store_name'
ATTR_ENTITY_ID  = 'entity_id'

# Select light attributes to save/restore.
ATTR_BRIGHTNESS = "brightness"
ATTR_EFFECT = "effect"
ATTR_WHITE_VALUE = "white_value"
ATTR_COLOR_TEMP = "color_temp"
ATTR_HS_COLOR = "hs_color"
# Save any of these attributes.
GEN_ATTRS = [ATTR_BRIGHTNESS, ATTR_EFFECT]
# Save only one of these attributes, in order of precedence.
COLOR_ATTRS = [ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, 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 overwrite parameter (only applies to saving.)
    overwrite = data.get(ATTR_OVERWRITE, True)

    # 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(',')]

    # Replace any group entities with their contents.
    # Repeat until no groups left in list.
    expanded_a_group = True
    while entity_id and expanded_a_group:
        expanded_a_group = False
        for e in entity_id:
            if e.startswith('group.'):
                entity_id.remove(e)
                g = hass.states.get(e)
                if g and 'entity_id' in g.attributes:
                    entity_id.extend(g.attributes['entity_id'])
                    expanded_a_group = True

    # 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:
        # Only save if not already saved, or if overwite is True.
        if not saved or overwrite:
            # 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.') and cur_state.state == 'on':
                        for attr in GEN_ATTRS:
                            if attr in cur_state.attributes and cur_state.attributes[attr] is not None:
                                attributes[attr] = cur_state.attributes[attr]
                        for attr in COLOR_ATTRS:
                            if attr in cur_state.attributes and cur_state.attributes[attr] is not None:
                                attributes[attr] = cur_state.attributes[attr]
                                break
                    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 and old_state.attributes:
                    service_data.update(old_state.attributes)
                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)

Yes here is…

- alias: "Good Night House"
  initial_state: 'on'
  trigger:
  - at: '20:44:59'
    platform: time
  action:
    - service: script.restore_lights_on
    - service: switch.turn_off
      entity_id: switch.esszimmer_lampe, switch.kasten
    - service: light.turn_off
      entity_id: 'all'
    - service: script.restore_lights

Is the problem still happening now that you are using 1.3.0?

Yes, unfortunately… and it doesn’t work to restore the lamps, etc…
All remain switched off even though they were on before the script.

Please test calling the script manually (from Developer Tools → Services.)

After the save operation, look in Developer Tools → States. You should see “entities” whose IDs start with the store name (i.e., “flash_store.”). There should be one for each light or switch whose state is being saved. Do they look correct / as you would expect? Please share what you get.

Then do the same but using the restore operation. Do you get errors when you do that? Look in Developer Tools → States. Have all the “flash_store.xxx” rows been deleted?

1 Like

yes, IDs are created and deleted again, everything seems to be working but the log says this…

Logger: homeassistant.components.homeassistant
Source: components/homeassistant/__init__.py:86
Integration: Home Assistant Core Integration (documentation, issues)
First occurred: 16:35:16 (1 occurrences)
Last logged: 16:35:16

The service homeassistant.turn_on cannot be called without a target

and everything stays turned off in the list…

Addendum!

I just discovered that if you call the two scripts manually one after the other everything works, but in the automation with each other, the restore_lights IDs remain!

The funny thing is that this completely stupid nonsense works with a second automation

give up, leave it like that now at least everything is finally working again

################################################################################################    
- alias: "Good Night House"
  initial_state: 'on'
  trigger:
  - at: '20:44:58'
    platform: time
  action:
  - service: script.restore_lights_on
  - service: switch.turn_off
    entity_id: switch.esszimmer_lampe, switch.kasten
  - service: light.turn_off
    entity_id: 'all'

- alias: "Good Night House Restore"
  initial_state: 'on'
  trigger:
  - at: '20:45:00'
    platform: time
  action:
  - service: script.restore_lights
      
################################################################################################

python_script.light_store never calls that service (i.e., homeassistant.turn_on), so that error must be from something else failing, possibly preventing python_script.light_store from running somewhere in your automation or script.

I think you’re misinterpreting what is causing that error. Have you tried looking at the traces for your automations & scripts?

His problem is that he’s sending too many commands to his WLED device. If you look at the root of the error, it points to a json decode error w/ WLED. The HA WLED integration is notorious for causing crashes on WLED devices. So much so, that I avoid automating my WLED devices beyond simple on/off.

If he’s restoring states and effects for multiple WLED segments, there’s a really high chance that he’s crashing his device and this error is a result of that crash.

Or it’s a simple wled integration or json response (from wled) bug.

1 Like

@pnbruckner I’ve submitted an issue…strange one for me… Error when trying to save lights state · Issue #165 · pnbruckner/homeassistant-config · GitHub

Weird, for some reason github did not notify me when you opened that issue. Thanks for letting me know.

Anyway, I have no idea what’s going on with that. I did a google search and the first thing that came up was a Home Assistant Core issue:

More searching finds many other (unrelated to HA) instances of this. AST is a standard Python package. I doubt this has anything specifically to do with this HA python_script.

OK thanks. Weird thing is that it happened to me only once…anyway I will keep my eye on that.