Pyscript! are you using it?

If you aren’t using pyscript already, you probably should be.

It installs as a custom component and makes writing automations that would normally take several different automations and many lines of template code and “choose” actions quite easy with just a little Python.

It’s similar to AppDaemon in that you’re writing your Automations in Python. However, it doesn’t require a separate process, an API Key, or any YAML if you don’t want it to.

The syntax is quite simple and @craigb has crafted it so that variable names are what you’d expect in Home Assistant. For instance, want to turn on a light when motion is detected?

if binary_sensor.motion == 'on':
   light.turn_on(entity_id="light.overhead")

It also has quite a few advanced features – like packaging code into an “app” to be used by others with just a little YAML to configure it. So, if you make something useful (like my conditional average app) you can share it and no one else ever has to write that code again. In a way, it’s like the “Apps” that SmartThings has, only in Python, and specifically for Home Assistant.

Give it a shot. And if you get stuck on something, I’m happy to help.

4 Likes

I read this thread pretty much when you posted it and since then I have tried to understand what this is, but failed. That is to say that I understand the concept (use python to make better, more ‘program oriented’ automations), but I don’t understand where it sits.

I think many more people would use this if the documentation was a bit easier to follow. I like to think that I’m quite good at following things generally, but by the time I got to like the third page I was completely lost.

Things like “it’s highly recommended to do X” without the slightest hint of what the benefits/pitfalls of X might be add to enduser confusion, and the very first example looks like it’s expecting to receive the arguments ‘turn on’ and an entity_id, but the next instruction is to send it the arguments ‘hello’ and ‘world’ which will presumably do nothing, which seems a pointless exercise. There is no explanation of what the result of sending it ‘hello’ and ‘world’ should actually be.

Then the next page may as well be written in swahili tbh…

@state_trigger describes the condition(s) that trigger the function (the other two trigger types are @time_trigger and @event_trigger, which we’ll describe below).

So, is it a trigger or is it a condition?

This condition is evaluated each time the variables it refers to change

What variables?

and if it evaluates to True or non-zero then the trigger occurs.

If what evaluates to true? The trigger? The condition? The variable? :man_shrugging:

A decent, simple walk through of a real world example of doing something would probably be enough to help the mist clear, but as much as I want to understand it and play with it, I’m going nowhere fast and I suspect I’m not the only one.

I totally understand. Documentation is the key to any good project. I feel like @craigb has done a pretty good job documenting HOW pyscript works, but maybe some more effort could be put into documenting WHY one would want this and into showing some real world examples. I think the second example in the tutorial is a pretty good “real world” example, though.

To give more direct responses to your questions about state_trigger

It is both. Or, in Home Assistant lingo, it is similar to a template trigger (which is a trigger that evaluates a template that returns true or false). These do the same thing:

In a Home Assistant Automation:

- alias: some automation
  trigger:
    platform: template
    value_template: "{{ is_state('binary_sensor.test', 'on') }}"
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In pyscript:

@state_trigger('binary_sensor.test == "on"')
def turn_on():
  homeassistant.turn_on(entity_id='switch.test')

I hope that helps a bit. Either way, I’ll see if I can add some additional documentation on WHY you would want this and additional Home Assistant equivalents in to pyscript Wiki.

1 Like

Yeah, I think that’s the key to it. Using homeassistant equivalent terms would definitely help me wrap my head around it, much the same using a homeassistant term in a non homeassistant way makes it a bit more confusing.

I will definitely spend some more time trying to get my head around it, it does seem like a really powerful tool, but just wanted to express how difficult it looks on first (and second, and third…) glance for me :slightly_smiling_face:

It’s basically a replacement for the HA automations but in python. It seems easier to learn/implement than appdeamon too.

The trouble with Home Assistant “terms” is that they don’t always work outside of home assistant. Like the word “template” in “template trigger”. There is no “template” in pyscript’s version of a “template trigger”.

In pyscript, you have a piece of code like this:

binary_sensor.test == 'on'

This is an expression in Python. It will evaluate to “True” or “False”, a lot like the template part of a template trigger. You can use that code anywhere, including in a state_trigger. But it could be anywhere. For instance, say you wanted to turn on a light when you see motion but only when it’s dark. Just like in a Home Assistant Automation, there are MANY different ways to do this in pyscript. But here’s one:

@state_trigger('binary_sensor.motion == "on"')
def turn_on_if_dark():
  if binary_sensor.dark == 'on':
    light.turn_on(entity_id='light.front_porch')

But, like I said, there are many ways to do the same thing, just like in Home Assistant. So you could also write it like this:

@state_trigger('binary_sensor.motion == "on" and binary_sensor.dark == "on"')
def turn_on_if_dark():
  light.turn_on(entity_id='light.front_porch')

Of course, just like in Home Assistant Automation, these two ARE slightly different. In the second version, if there is motion and THEN it gets dark while the motion is still happening, the light will still turn on. In the first example, if it’s not already dark when the motion starts, then the light will not turn on.

Or maybe you just want the light on when it’s dark, regardless of motion:

@state_trigger('binary_sensor.dark == "on"')
def turn_on_if_dark():
  light.turn_on(entity_id='light.front_porch')

Pyscript handles template triggers, state triggers, and numeric state triggers all with @state_trigger. Event triggers are handled with @event_trigger and any of the time based triggers (like the sun trigger, time trigger, and time_pattern trigger) are handled with @time_trigger.

The “action” part of your home assistant automation is what follow the def whatever(): portion in pyscript. This is just a python function that will be run when the trigger happens.

Of course, you can do some fairly complex things pretty easily when you don’t have to write it in YAML.

Here’s a real world example (i.e. I actually use this) of a pyscript automation. It adjusts an input_number representing an away temperature based on an input_number that is the desired temperature as well as the distance away from my home that the closest person is. The idea is, the farther away from home we are, the hotter/colder we can adjust the temperature since, as we drive closer the temperature will adjust back down and be exactly what we want when we finally arrive at home.

@state_trigger('True or climate.downstairs or proximity.all or input_number.desired_temperature or input_number.away_temperature')
@time_trigger('startup')
def climate_away_diff():
    desired_temp_entity = 'input_number.desired_temperature'
    set_entity = 'input_number.away_temperature'
    cool_base = 3
    heat_base = -4
    max_diff = 10
    min_diff = -10
    cool_coeff = (1 / 10)
    heat_coeff = (-1 / 10)
    distance = float(proximity.all)

    desired_temp = float(state.get(desired_temp_entity))
    away_temp = float(state.get(set_entity))

    climate_state = climate.downstairs

    if climate_state == 'cool':
        diff = cool_base + (cool_coeff * distance)
    elif climate_state == 'heat':
        diff = heat_base + (heat_coeff * distance)
    else:
        diff = 0

    if diff > max_diff:
        diff = max_diff

    if diff < min_diff:
        diff = min_diff

    new_temp = desired_temp + round(diff)

    if new_temp == away_temp:
        log.debug(f'Away Temp already set to {new_temp}')
        return

    log.info(f'Setting Away Temp to {new_temp}')

    input_number.set_value(
        entity_id = set_entity,
        value = new_temp
    )

I could try to write this in a Home Assistant Automation, but it would require some painful amounts of template code that just isn’t fun to read or write. But, if you’re having a hard time understanding what it’s doing, I’d be happy to try writing the Home Assistant equivalent so you can see the difference.

can you make your own decorators?

I get that, but that’s why I mentioned that using homeassistant terms in contrast to how homeassistant uses them makes it more confusing.

If we consider that the word ‘trigger’ in both senses means “I want to react to this…” then @state_trigger makes perfect sense that we’re reacting to a state change, but the explanation of @state_trigger in the docs says this is a condition (which in homeassistant terms is “I don’t want to react if this is true”) which leads to immediate confusion.

To use your first example…

@state_trigger('binary_sensor.motion == "on"')
def turn_on_if_dark():
  if binary_sensor.dark == 'on':
    light.turn_on(entity_id='light.front_porch')

Seems like line 1 is the trigger and line 3 is the condition (in homeassistant parlance), and whilst that doesn’t necessarily translate (as the ‘condition’ is actually more like a templated action), I can get the concept of that easier now that I have understood that the word condition in the state_trigger was not a ‘homeassistant term’. The problem being that using that term in a component that is designed for homeassistant means that the reader expects it to mean what it means in homeassistant, which I think is where I started to get lost.

I hope that makes some sense.

Thanks for the bigger ‘real world’ example, that’s quite easy to follow and I mostly can see how it all slots together - although the @state_trigger ‘True or…’ has confused me. What is that checking?

That is a weird one that I don’t particularly care for, myself. The author explains it about half way through the state_trigger documentation.

In short, a state_trigger, as previously discussed, is both a trigger, and a condition – just like a template trigger. And, also like a template trigger, the code that powers each of these looks in the template (in Home Assistant) or the condition expression (in pyscript) to find which entities to look for changes in.

So we have, more simply, this:

@state_trigger('True or binary_sensor.dark')

Which is equivalent to this:

- alias: some automation
  trigger:
    platform: state
    entity_id: binary_sensor.dark

We’re including “binary_sensor.dark” in the expression so that the code that looks for entities in that expression will see that it should evaluate the expression again whenever “binary_sensor.dark” changes. But, we don’t care what the state of “binary_sensor.dark” is. We want the function to run on any change. So we start the expression with “True” so that it will ALWAYS be “True” and therefore trigger the function.

It’s hacking looking, I agree. Pyscript is quite new so the syntax doesn’t have all the rough edges filed down yet. But I’ll be issuing a feature request shortly for a better way to represent this.

1 Like

Why is this:

    distance = float(proximity.all)

not this?

    distance = float(state.get('proximity.all'))

Or is it an abbreviated way of representing the same thing?

Full disclosure: I’m new to pyscript so I may be asking an ignorant question …

And you are spot on here. And if statement inside of the function is roughly equivalent to a “condition” in the “action” part of a home assistant automation. It’s more powerful than that, because an if can have an else. So it’s even more like the recently added “choose” functionality in a Home Assistant “action” / “script”.

It’s an abbreviated way of representing the same thing. I used state.get() on the others because they were set in a variable and state.get() is the way to get the state of an entity that is stored in a variable. Each of these values of dark will be the same:

dark = state.get('binary_sensor.dark')

dark = binary_sensor.dark

dark_sensor = 'binary_sensor.dark'
dark = state.get(dark_sensor)
1 Like

For the curious, I had converted someone’s python_script to pyscript in this post. It was an exercise to help me understand the differences between the two. It also demonstrated how pyscript’s syntax is less verbose than python_script (with the same or better legibility … once you get accustomed to its syntax).

In the same spirit, I’ve converted swiftlyfalling’s pyscript example into a YAML automation. Once again it demonstrates how pyscript’s syntax allows for a less verbose end-result.

- alias: Climate Away Diff
  trigger:
  - platform: state
    entity_id: climate.downstairs
  - platform: state
    entity_id: proximity.all
  - platform: state
    entity_id: input_number.desired_temperature
  - platform: state
    entity_id: input_number.away_temperature
  - platform: homeassistant
    event: start
  action:
  - service: input_number.set_value
    data:
      entity_id: input_number.away_temperature
      value: >
        {% set desired_temp_entity = 'input_number.desired_temperature' %}
        {% set set_entity = 'input_number.away_temperature' %}
        {% set cool_base = 3 %}
        {% set heat_base = -4 %}
        {% set max_diff = 10 %}
        {% set min_diff = -10 %}
        {% set cool_coeff = (1 / 10) %}
        {% set heat_coeff = (-1 / 10) %}
        {% set distance = states('proximity.all') | float %}

        {% set desired_temp = states(desired_temp_entity) | float %}
        {% set away_temp = states(set_entity) | float %}

        {% set climate_state = states('climate.downstairs') %}

        {% if climate_state == 'cool' %}
          {% set diff = cool_base + (cool_coeff * distance) %}
        {% elif climate_state == 'heat' %}
          {% set diff = heat_base + (heat_coeff * distance) %}
        {% else %}
          {% set diff = 0 %}
        {% endif %}

        {% if diff > max_diff %}
          {% diff = max_diff %}
        {% endif %}

        {% if diff < min_diff %}
          {% diff = min_diff %}
        {% endif %}

        {{ desired_temp + round(diff) }}

It’s possible to streamline the template slightly but I chose not to in order to make it easier to compare syntax with the pyscript example.

NOTE

I must point out that, this is not 100% faithful to the pyscript example. I excluded the two logging calls because they would be completely separate service calls in a YAML automation.

That’s an advantage of using pyscript (and python_script): you can make any number of service calls within the body of the code. In a YAML automation, you are coding for each service call. So if the code (template) for one service call is needed for another call, you will have to repeat it.

4 Likes

Oh no, another super compelling automation tool to distract me from the three I’m using now! Sign me up! :grinning:

2 Likes

Gah. I hate posts like this. It’s not bad enough that I’m using NodeRed, native automations, and AppDaemon… Now I have to throw this in the mix?! Shame on you @swiftlyfalling.

Seriously though, fantastic write up and I love that example that you put up. Now I have another tool to add to the automations wheelhouse.

2 Likes

FWIW, I recently learned a clever way to limit a value to fall within a specified range.

You can replace this:

    if diff > max_diff:
        diff = max_diff

    if diff < min_diff:
        diff = min_diff

with this:

    diff = sorted([min_diff, diff, max_diff])[1]

Screenshot from 2020-10-15 12-22-24


EDIT

Equivalent in Jinja2:

Screenshot from 2020-10-15 12-27-09

3 Likes

The biggest concern i have about it, as it’s a custom_component.
Will this be future proof?

1 Like

Is anything really ever future proof? I mean, look at the changes to automations just between 0.114 and 0.116. Or the changes between AppDaemon3 and AppDaemon4.

Personally, I don’t ever bank on anything in technology to ever be 100% future proof.

1 Like

On the up side, it uses very few parts of HASS that tend to change.

On the down side, if the author abandons it, I don’t grok Ast and GlobalContexts enough to fix it myself. So I guess I’d have to learn. :slight_smile:

1 Like

Anyone else with this issue?