Pyscript! are you using it?

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?

Me! Just wasn’t sure it was Pyscript.

Hy Daniel,

isn’t it possible to use subfolders in pyscript?
If i move my script in a subfolder it doesn’t work anymore.

yes and no…

you can use pyscript/apps/anything/__init__.py as long as you set it up as an app with YAML like this:

pyscript:
  apps:
    app_name: {}

From inside of an app you can import other files in that same app package.

You can import files in other directories. Though this may be limited to files in the pyscript/modules directory, I haven’t played with it much since the feature was added.

Check out this documentation for more information on importing.

But, no, if you want to have pyscript autoload files in subdirectories of pyscript it doesn’t do that. Only the root pyscript/ directory. Make a Feature Request PR with your use case though, if it’s something you want. Craig is quite open to change requests.

Thanks for the explanation.

So on to another question. :slightly_smiling_face:

With
task.unique("my_function_name")
the task is terminated if it was previously called.

Is it possible to test if there’s a previously called task or if it’s ‘the first run’?

Not anything that I’m aware of.

And remember, it doesn’t check if it’s every been called before. It checks if there is a task actively running. Aside from very complex tasks and race conditions, this is likely only to happen if you are also calling task.sleep() or using some other async method that take time. Otherwise, generally, the task starts and stops so quickly it won’t still be running.

What exactly are you trying to do?

Well, hard to explain with my limited english.
Here’s my script. I commentent it in there.
Hope this makes sence, and thanks very much for your assistance.

@state_trigger("binary_sensor.motion_group == 'on'")
@state_active("input_boolean.night_time == 'on' and input_boolean.wz_desk_motion_light == 'on'")

def wz_desk_motion_light():

    # The light should go on if it's darker than the setting in the input number
    # But when the light comes on, it gets brighter than the input setting, so
    # this will never pass the if statement again, and the light gets off
    # even though there is still motion

    if float(sensor.bh1750_illuminance) < float(input_number.motion_light_sonoff_01):
        # or there's already a running task <- THIS is what i'm tryiing to do
        
        task.unique("wz_desk_motion_light")

        if light.schreibtisch_gu10 != "on":

            x = sensor.random_color.split(',')[3]
            y = sensor.random_color.split(',')[4]

            light.turn_on(entity_id="light.schreibtisch_gu10",
                        brightness=140,
                        xy_color=[x,y])

        task.sleep(float(input_number.motion_timer_sonoff_01))

        light.turn_off(entity_id="light.schreibtisch_gu10")

I did this in yaml with a script and testet if that script was already running.

So, every time binary_sensor.motion_group turns on, you want to turn the lights on again and start the “sleep” over again?

There’s a couple of ways…

You can use a global variable outside of the function to indicate that you’re in an “on” loop. Like this:

motion_light_on = False

@state_trigger("binary_sensor.motion_group == 'on'")
@state_active("input_boolean.night_time == 'on' and input_boolean.wz_desk_motion_light == 'on'")

def wz_desk_motion_light():
    global motion_light_on

    if motion_light_on or float(sensor.bh1750_illuminance) < float(input_number.motion_light_sonoff_01):
        
        task.unique("wz_desk_motion_light")

        if light.schreibtisch_gu10 != "on":

            x = sensor.random_color.split(',')[3]
            y = sensor.random_color.split(',')[4]

            light.turn_on(entity_id="light.schreibtisch_gu10",
                        brightness=140,
                        xy_color=[x,y])

        motion_light_on = True
        task.sleep(float(input_number.motion_timer_sonoff_01))
        motion_light_on = False

        light.turn_off(entity_id="light.schreibtisch_gu10")

Or, if you always want to do this when that particular light is on, you can just check if it’s on:

@state_trigger("binary_sensor.motion_group == 'on'")
@state_active("input_boolean.night_time == 'on' and input_boolean.wz_desk_motion_light == 'on'")

def wz_desk_motion_light():
    if light.schreibtisch_gu10 == "on" or float(sensor.bh1750_illuminance) < float(input_number.motion_light_sonoff_01):
        
        task.unique("wz_desk_motion_light")

        if light.schreibtisch_gu10 != "on":

            x = sensor.random_color.split(',')[3]
            y = sensor.random_color.split(',')[4]

            light.turn_on(entity_id="light.schreibtisch_gu10",
                        brightness=140,
                        xy_color=[x,y])

        task.sleep(float(input_number.motion_timer_sonoff_01))

        light.turn_off(entity_id="light.schreibtisch_gu10")
1 Like