Pyscript - new integration for easy and powerful Python scripting

Sorry, I’m not that familiar with HACS. Do you have the “Enable newly added entities” option turned on in HACS’ configuration?

As I understand it, HACS will only show integrations in the search (unless added as a custom repository) if they are listed here:

When I wanted to add one of mine as default, I needed to issue a PR

Thanks! I incorrectly thought that https://github.com/custom-components were automatically included, but it appears they are not. I’ll go ahead and submit a PR to get it updated.

So I’m having an issue converting some of my automations over. I keep getting a syntax error when attempting to do an if statement using a binary variable. I have other if statements that work and I can output the variable so I believe I’m calling it correctly.

Here is the code in question https://pastebin.com/Ky7gM2RP

and this is the error

Logger: homeassistant.components.pyscript.eval
Source: custom_components/pyscript/eval.py:918
Integration: Pyscript Python scripting (documentation, issues)
First occurred: 8:53:09 PM (1 occurrences)
Last logged: 8:53:09 PM

syntax error file /config/pyscript/thermostat_day.py: Traceback (most recent call last): File "/config/pyscript/thermostat_day.py", line 9 if binary_sensor.workday == 'on' ^ SyntaxError: invalid syntax

I have another automation that replicates most of that (without the workday check) for night time that is working fine so that part works it seems to just be the check for the workday sensor (and I copied that name directly from HA so it should be correct).

Found my issue I was missing a : at the end of the if

it wasnt that hard to see because the error told it was a syntaxerror on line 9 :wink:
but at start all errors are confusing :wink:

i think next time you got a syntaxerror you will know that you probably made a typo somewhere, so it was a usefull error.

(and me remarking it will hopefully help that its even more praqued into your mind :wink: )

Glad you found the error. I noticed one more issue - the very last line that does the service call likely needs to be indented by 4 spaces so that it is the last line of the function. Currently (with indent 0) it is not part of the function, and only executed once when the entire file is first loaded and executed (ie, on startup or reload).

Also, for testing, you could temporarily replace the @time_trigger with @service, so you can manually call that service (ie: make a service call pyscript.thermostat_work_day from the UI to test the function). Then once it is working you can go back to the @time_trigger, reload it, and check tomorrow morning, or make the @time_trigger shortly in the future to test it, so you don’t have to wait that long.

Yeah I did the service trick to check the rest of the logic (when coding the night version which doesn’t have the extra if) and just didn’t think to check the last bit with the service so I didn’t notice that syntax error and only noticed cause I was working on the occupancy automation for the thermostat (using state_triggers and a group of persons)

As for the last line I’m not sure what happened in the pastebin it is indented correctly on my copy

Now that I have these working (need to properly test the occupancy one which is hard with lock down, but I’ve toggled the state manually so I believe it should work) I’ll probably put some examples up on the wiki.

Great - sounds like you’ve made a lot of progress, which is good to hear.

One comment about the error message. If you look in the log file, there will be a multi-line error message where the caret “^” points exactly to the error, eg:

2020-08-07 18:49:44 ERROR (MainThread) [homeassistant.components.pyscript.eval] syntax error file /config/pyscript/scripts.py: Traceback (most recent call last):
  File "/config/pyscript/scripts.py", line 40
    if xyz.abc
             ^
SyntaxError: invalid syntax

I guess in the logger output all that whitespace gets collapsed, so you can’t tell where on the line the error is. In those cases you could look in the log file to be sure.

I am stuck attempting to retrieve an attribute of an entity. The entity_id is passed to the pyscript function from the service menu (ie. light_group: group.dining_room_lights).If I attempt to access the attribute directly (ie. group.dining_room_lights.entity_id) I get the list of entity ids.

Using state.get(light_group) returns off. How do I get the entity_id attribute instead?

Brian,

Since the state variable name (group.dining_room_lights) you want is the value of a variable (light_group), you have to build the string name with the attribute appended, and use state.get:

state.get(f"{light_group}.entity_id")

The argument will be a string that evaluates to "group.dining_room_lights.entity_id", and then state.get() will return the value of that attribute.

Another way to write that which looks cleaner is:

state.get(light_group + ".entity_id")

Craig

1 Like

Thank you @craigb.

I am working on creating a function which can sequence through a group of colored smart bulbs (similar to what the Hue app does). (For reference my dining room chandelier has 9 hue bulbs.)

@service
def light_sequence(light_group=None, delay=1, transition=5, colors=["red","purple"], brightness=255):
  """Light sequencer to animate the colored bulbs."""
  #log.info(f"light_sequence({light_group}, {delay}, {transition}, {colors}, {brightness})")

  if light_group is not None:
    # Retrieve the list of entity ids in the light group.
    bulbs = state.get(light_group+".entity_id")

    # Loop through the list of colors.
    for color in colors:
      # Loop through the list of bulbs.
      for bulb in bulbs:
        # Stop when the lights are turned off.
        if state.get(light_group) is "off":
          break

        # Brief delay inbetween bulbs.
        task.sleep(delay)

        # Transition the bulb to the new color.
        light.turn_on(entity_id=bulb, color_name=color, transition=transition, brightness=brightness)

      # Stop when the lights are turned off.
      if state.get(light_group) is "off":
        break

      # Delay for the duration of the color transition.
      task.sleep(transition)

To test it I call pyscript.light_sequence with the services page with the following data:

light_group: group.dining_room_light
brightness: 60
delay: .75
transition: 5
colors:
  - aquamarine
  - blueviolet
  - cadetblue
  - coral

The code works well. My ZHA network doesn’t reliably send the command to every bulb, but that’s a problem for another forum. :slight_smile:

P.S. In case you are curious, here are links to the 2 Home Assistant script files I converted this from:

4 Likes

Brian,

Thanks for posting the code - looks really cool! It’s helpful to compare to the corresponding script files.

One comment is that you should use == instead of is when you are checking for "off". The is operator checks if the operands are the same object, not just that they have equal values. So to check that the light is off you should use

if state.get(light_group) == "off":

Craig

1 Like

Thanks @craigb.

How can I use pyscript to stop the script from running when someone turns the light group off. The break statements inside each loop isn’t working.

Brian,

You should add this call

task.unique("light_sequence_running")

at the top of your light_sequence function. That will ensure that if a second service call starts a new light sequence, then the old one will be terminated.

Next, create a new state-trigger function that triggers if the light is turned off. That function should do one thing: also call task.unique("light_sequence_running"). That will cause any existing light_sequence function to be terminated (because it previously called task.unique()). Perhaps it would be something like:

@state_trigger("group.dining_room_light == 'off'")
def dining_room_maual_off():
    task.unique("light_sequence_running")

However, this will stop any light_sequence that is running, even if it’s for a different light group.

Here’s an improvement that probably is better for your application. Since your light_sequence function can run on any light group, you actually want task.unique to only apply to a service function running on that same light group. So you should make the string argument to task.unique (which can be any string) be based on light_group. You probably want to use something like:

task.unique("light_sequence_running_" + light_group)

and you’ll need that same argument in the “manual off” trigger to make sure it stops only the matching service.

For the manual off state trigger to work across multiple groups, you’ll need to list all the light groups that could turn off in the @state_trigger, and use the var_name argument to figure out which light group was turned off (it’s the name of the state variable that just changed to cause the trigger, which is the light group). For example, for two light groups, you could do:

@state_trigger("group.dining_room_light == 'off' or group.living_room_light == 'off'")
def light_group_manul_off(var_name=None):
    task.unique("light_sequence_running_" + var_name)

Unfortunately you can’t use wild-cards in a state_trigger - it needs an explicit list of state variables to watch.

Craig

2 Likes

As an experiment, I converted your pyscript example to python_script to demonstrate the syntactic differences between the two. During this exercise, I encountered the same issue you did, it failed to break out of the loop when the light group was turned off.

To correct it, I employed the technique described in this StackOverflow post: python - Breaking out of nested loops - Stack Overflow

Here’s the python_script version (confirmed it works):

# python_script
light_group = data.get('light_group', None)
delay = data.get('delay', 1)
transition = data.get('transition', 5)
colors = data.get('colors', ["red","purple"])
brightness = data.get('brightness', 255)

if light_group is not None:
  bulbs = (hass.states.get(light_group)).attributes['entity_id']
  for color in colors:
    for bulb in bulbs:
      if (hass.states.get(light_group)).state == 'off':
        break
      time.sleep(delay)
      hass.services.call('light', 'turn_on', {'entity_id': bulb, 'color_name': color, 'transition': transition, 'brightness': brightness}, False)
    else:
      if (hass.states.get(light_group)).state == 'off':
        break
      time.sleep(transition)
      continue
    break

I applied the same technique to your pyscript in the example shown below. However, I have not tested it because my Hue lights are connected to my production server and I’m not running pyscript on it (yet). Comparing the syntax of the two examples, it’s clear the pyscript version provides simplified access to an entity’s state and attributes.

#pyscript
@service
def light_sequence(light_group=None, delay=1, transition=5, colors=["red","purple"], brightness=255):
  if light_group is not None:
    bulbs = state.get(light_group+".entity_id")
    for color in colors:
      for bulb in bulbs:
        if state.get(light_group) == "off":
          break
        task.sleep(delay)
        light.turn_on(entity_id=bulb, color_name=color, transition=transition, brightness=brightness)
      else:
        if state.get(light_group) == "off":
          break
        task.sleep(transition)
        continue
      break

Let me know if it works or not.

2 Likes

@123 - ah good point - a break statement only terminates the innermost loop. However, @BrianHanifin’s code does the same check in the outer loop too.

Perhaps the issue is the check should be after the delay (immediately before it is turned on), rather than before? Or you could check before and after, so that you don’t waste time on the delay if the light was already off. There’s obviously a race condition between when the check occurs and the turn_on service is called relative to when the manual switch off happens. It’s not possible to eliminate that, but you want it to be as short as possible.

Craig

1 Like

After thinking on it some more, the breaks probably don’t work due to my bigger problem. My light groups turn right back on when even one of the bulbs doesn’t report to ZHA that it turned off when the command was sent.

Thank you @123 I am enjoying learning Python. I will try those soon.

There is an else in the pyscript code that looks out of place. It is lined up with the for bulb in bulbs: statement. Help me out. I’m not sure what is supposed to line up with what.

It’s not out of place.
https://book.pythontips.com/en/latest/for_-_else.html#else-clause

Like I said, I got the idea from the linked StackOverflow post. It’s a method for breaking out of any number of nested for-loops.

Ultimate proof that it’s not out of place is that it simply works; when I turn off the light group the looping ceases.

Having said that, with 9 lights in your group, YMMV but the technique is sound.

1 Like

I finally got around to adding all the “test” scripts that I made to figure out time triggers / state triggers / service calls while I was redoing my thermostat last month (and realized I don’t account for my vacation variable) to the wiki (under examples)

I also added the more complex thermostat scripts so others can use them as examples.

Probably work on converting my light scripts next.

I just noticed that the log.info does not seem to be showing up in my logs any more. I don’t believe that I changed anything since I coded all my other ones (and made extensive use of the log while testing so it was working then).