Pyscript! are you using it?

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

Thanks very much!
I think the first example with the global variable is the way to go, because the light could be already manually switched.
Will try it later.

I just committed code that makes “trigger on any change” much cleaner, per @swiftlyfalling’s feature request.

Now (using the master version), you can just do

@state_trigger('binary_sensor.dark')

and it will trigger on any change to that variable. @state_trigger can now take multiple arguments, each of which can be a string or list of strings. All of them are logically “or”'ed together.

1 Like

There’s also:

diff = max(min_diff, min(max_diff, diff))
1 Like

I have read quite a bit in the documentation and in the examples, but i don’t understand how to use apps for multiple entities.
Take one simple example,

@state_trigger("binary_sensor.room1 == 'on'")
def motion_light():
    task.unique("motion_light")
    if light.room1 != "on":
        light.turn_on(entity_id="light.room1")
    task.sleep(float(input_number.room1))
    light.turn_off(entity_id="light.room1")

How should that look as an app if i use a config like this for multiple rooms.

pyscript:
  apps:
    motion_lights:
      - room: room1
        motion_id: binary_sensor.room1
        light_id: light.room1
        time_id: input_number.room1
      - room: room2
        motion_id: binary_sensor.room2
        light_id: light.room2
        time_id: input_number.room1

Thanks in advance.

1 Like

So the trick with making an app like this work as an “app” in pyscript is closures. Put simply, you’ll be be writing functions that make functions. It sounds complicated, but it can be made pretty simple. I’ll convert your “script” above to an “app” and hopefully, it’ll click. But, if it doesn’t, I’m happy to answer more questions:

# we'll use this later
registered_triggers = []

# First, we define a function that makes more functions.
def make_motion_light(config):

    # this is the code you wrote, with a few changes to allow
    # for substituting in the variables you have in your YAML
    # compare it line by line with your original code

    # replace the hardcoded entity ID with the value from config
    @state_trigger(f"{config['motion_id']} == 'on'")
    def motion_light():

        # we want a unique task for each separate app, so lets use
        # a unique name based on some config data
        task.unique(f"motion_light_{config['motion_id']}")

        # because our light entity is in a variable we'll have to
        # use the longer form to get the state
        if state.get(config['light_id']) != "on":

            # substitue in the value from config
            light.turn_on(entity_id=config['light_id'])

        # substituting again   
        task.sleep(float(state.get(config['time_id'])))

        # substitute from config
        light.turn_off(entity_id=config['light_id'])


    # now that we've made a function specifically for this config item
    # we need to register it in the global scope so pyscript sees it.
    # the easiest way to do that is add it to a global list.
    registered_triggers.append(motion_light)


# now we need some code that runs at startup and processes the YAML configuration
# I've written a function for this that you can paste anywhere.
def load_apps(app_name, factory):
    if "apps" not in pyscript.config:
        return
    
    if app_name not in pyscript.config['apps']:
        return

    for app in pyscript.config['apps'][app_name]:
        factory(app)

# now we just need the startup trigger
@time_trigger('startup')
def motion_light_startup():
    load_apps("motion_lights", make_motion_light)
2 Likes

Thanks so much Daniel!
That’s defintely something to start with. :slightly_smiling_face:

I adapted the code with some other entities and the global vars from your last post.

pyscript:
  apps:
    motion_light:
      - room: room1
        main_switch_id: input_boolean.wz_desk_motion_light # to switch the 'automation' off
        motion_id: binary_sensor.motion_group # motion sensor
        light_id: light.schreibtisch_gu10 # light to switch
        lux_id: sensor.bh1750_illuminance # illuminance sensor
        lux_set_id: input_number.motion_light_sonoff_01 # input_number to set min illuminance 
        time_id: input_number.motion_timer_sonoff_01 # input_number how long the light schould be on after last motion

config/pyscript/apps/motion_light.py

# we'll use this later
registered_triggers = []
global_vars = {}

# First, we define a function that makes more functions.
def make_motion_light(config):

    # this is the code you wrote, with a few changes to allow
    # for substituting in the variables you have in your YAML
    # compare it line by line with your original code

    global_vars[f"motion_light_{config['room']}"] = False

    # replace the hardcoded entity ID with the value from config
    @state_trigger(f"{config['motion_id']} == 'on'")
    @state_active(f"input_boolean.night_time == 'on' and {config['main_switch_id']} == 'on'")

    def motion_light():
        global global_vars

        # Light should go/stay on if it's darker then the lux setting from input number
        # or if the input_number = 0 (bypass lux setting)
        # or if it's already on, as long as there is motion.
        if (float(state.get(config['lux_id'])) < float(state.get(config['lux_set_id']))
            or float(state.get(config['lux_set_id'])) == 0
            or global_vars[f"motion_light_{config['room']}"]):

            # we want a unique task for each separate app, so lets use
            # a unique name based on some config data
            task.unique(f"motion_light_{config['room']}")
            #task.unique(f"motion_light_{config['motion_id']}")

            # because our light entity is in a variable we'll have to
            # use the longer form to get the state
            if state.get(config['light_id']) != "on":

                # substitue in the value from config
                light.turn_on(entity_id=config['light_id'])

            global_vars[f"motion_light_{config['room']}"] = True

            # build an entity_id from "room" and use long form state.get
            # to get the state of it    
            task.sleep(float(state.get(config['time_id'])))
            #task.sleep(float(state.get(f"input_number.{config['room']}")))

            # substitute from config
            light.turn_off(entity_id=config['light_id'])
            global_vars[f"motion_light_{config['room']}"] = False


    # now that we've made a function specifically for this config item
    # we need to register it in the global scope so pyscript sees it.
    # the easiest way to do that is add it to a global list.
    registered_triggers.append(motion_light)


# now we need some code that runs at startup and processes the YAML configuration
# I've written a function for this that you can paste anywhere.
def load_apps(app_name, factory):
    if "apps" not in pyscript.config:
        return
    
    if app_name not in pyscript.config['apps']:
        return

    for app in pyscript.config['apps'][app_name]:
        factory(app)

# now we just need the startup trigger
@time_trigger('startup')
def motion_light_startup():
    load_apps("motion_light", make_motion_light)
2 Likes

Are you sure about the difference?

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

I would think: trigger evalution, if there is motion, than check if it’s dark…

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

trigger; if there is motion AND it’s dark, than …

so, I would say, the first example, when there’s motion, and it gets dark right after the trigger condition is evaluated, the rest is executed…
in the second example, at the time the trigger condition is evaluated, both, motion AND dark have to be ‘on’… so, if one off them is not ‘on’, nothing happens…
So, it looks to me, your statement : “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.” is not correct… Or am I missing something?

In the first example, the only thing that can trigger the function is a change to “motion”.

So if it’s NOT DARK and motion happens, the trigger fires, but does nothing because of the first “if” checking to see if it’s dark. If motion says “ON” and dark then becomes “ON” this function will not trigger again because motion is still ON.

In the second example, both motion AND dark are triggers. So if either of them change, the entire string is evaluated again. So… if motion turns on, but dark is off, it never even triggers. if motion is still ON and dark turns ON, it’s evaluated again. This time, the condition is met, and the trigger function is run.

Agree… :slight_smile:

Someone used this?

from homeassistant.const import EVENT_CALL_SERVICE

@event_trigger(EVENT_CALL_SERVICE)
def monitor_service_calls(**kwargs):
    log.info(f"got EVENT_CALL_SERVICE with kwargs={kwargs}")

I tried it… And, I guess since EVERY event is logged, I needed to rename the script to #scriptname.py
and restart HA. because my log was filled and filled and filled… and HA became unresponsive…
This is een example found on : https://hacs-pyscript.readthedocs.io/en/latest/reference.html#firing-events

I think it’s meant as an example of HOW to use an event_trigger, not something that you should actually do (unless you’re on a test server with only a few entities, then it will be fine.)

yes, I think so too. :slight_smile:

wanted to try deconz.configure (a call of the service in pyscript), but I can’t get it to work fully.

deconz.configure(entity=id, field="/state", data={ “bri”: 254 , “on”: ‘true’ })

where id is a parameter… The data part is wrong. Has someone a few working examples?
I managed to get it work with “bri” only, but my goal was, "on:true, bri:254 and transitontime:20

UPDATE: found it… took me hours… :frowning:
deconz.configure(entity=id, field="/state", data={“bri”: 254 , “transitiontime”: 00, “effect”: “none”, “on”: bool(0)}) ==> works! no transiontime here, turn off light

deconz.configure(entity=id, field="/state", data={“bri”: 254 , “transitiontime”: 20, “effect”: “none”, “on”: bool(1)}) ===> works too! turn on light, with transition

the problem was the “bool”. True, true, “true” was not good… I thought also it was the maybe a problem with quotes or something else… But, found it :slight_smile: