Pyscript - new integration for easy and powerful Python scripting

Hi Folks,

I’m trying to automate my living space ventilation and have a problem with getting state variables within a @state_trigger

That is my code, it works generally as expected:

Status = state.get('switch.aus'), state.get('switch.tasmota_ac0bfbd9e600_switch_relay_3'), state.get('switch.tag'), state.get('switch.party') 
Status

The output is ('off', 'off', 'on', 'off'), which is the expeted state of the relais module in the working condition, right now.

Once I put it in a @state_trigger statement, it doesn’t work anymore:

@state_trigger('float(sensor.wohnung_co2) >= 1000 or float(sensor.lywsd03mmc_atc_hum) >= 60 or float(sensor.thx1_w230150x_hum_7) >= 60', watch='switch.aus, switch.tasmota_ac0bfbd9e600_switch_relay_3, switch.tag, switch.party')

     Status = state.get('switch.aus'), state.get('switch.tasmota_ac0bfbd9e600_switch_relay_3'), state.get('switch.tag'), state.get('switch.party') 
     Status

The output is:

Exception in <jupyter_26> line 2:
        Status = state.get('switch.aus'), state.get('switch.tasmota_ac0bfbd9e600_switch_relay_3'), state.get('switch.tag'), state.get('switch.party') 
       ^
IndentationError: unexpected indent (jupyter_26, line 2)

Edited because of a Typo which leads to a Syntax Error

Where is my mistake? I use the watch-statement with the needed entities as suggested in the documentation. Any ideas?

Kind regards Ulli

A decorator like @state_trigger must appear immediately before a function declaration. So you need to declare a function, and put the Status statement inside that function, like this:

@state_trigger('float(sensor.wohnung_co2) >= 1000 or float(sensor.lywsd03mmc_atc_hum) >= 60 or float(sensor.thx1_w230150x_hum_7) >= 60', watch='switch.aus, switch.tasmota_ac0bfbd9e600_switch_relay_3, switch.tag, switch.party')
def trigger_func():
     Status = state.get('switch.aus'), state.get('switch.tasmota_ac0bfbd9e600_switch_relay_3'), state.get('switch.tag'), state.get('switch.party') 
     Status
1 Like

I was hoping someone with better understanding might help me here. I’m trying to create an dynamic function witch fetches the sensors listed at the energy dashboard to do some corrections on readings.

Now one goal here is to dynamically decorate an function with state_trigger (to trigger on any new changes to the sensor value) for each sensor it finds “on the energy dashboard” - however this is where my knowledge stops. I don’t know or can’t find good resources on how to decorate functions on the fly as I go

“psuedo code”:

@time_trigger(“cron(*/1 * * * *)”)
def test():
sensors = [k.get(‘stat_consumption’) for k in hass.data[‘energy_manager’].data.get(‘device_consumption’)]

for sensor in sensors:
    state_trigger(sensor, my_func(var_name=sensor))

(where the function my_func(var_name= is the function which will do the corrections)

This obviously doesn’t fly well, but perhaps enough to shed light on what I’m hoping to achieve?

Hi, I really like pyscript! Such an amazing integration.

I am struggling a bit with triggering a input_datetime, could anyone point me in the right direction?
I want to trigger when current time is the same as input_datetime (hh:mm), this is my state trigger:

@state_trigger('input_select.housemode or input_datetime.heating_start or input_datetime.varme_heating_stop')
def my_function():

Right now, the function runs when I change input_datetime or input.select.
I want my function run when I make a change to input_select, or when input_datetime becomes the current time.
I tried using sensor.time == input_datetime also, with no luck.

Thanks for the help! I finally made it. I described my project (in German language) at this Hackpad (with the complete code) unter CC0 Licence: Automatisierung der Wohnraumbelüftung Stiebel Eltron LWZ 170 - HedgeDoc

It’s a small hardware hack of the Stiebel Eltron LWZ 170/270 central house ventilation and a descrition of the ideas and the workflows.

I used a ESP8622 with Tasmota and a 4 channel relay.

Feel free to take a look on it and reuse, if it is useful for you :slight_smile:

Here is an example of how I have done it. It will trig at the normal_bedtime each day

@time_trigger(f"once({input_datetime.normal_bedtime})")
def lys_av():
    if calendar.ferie == "on":
        script.turn_off_all_light.turn_on()
        media_player.media_stop(entity_id = "all")
1 Like

Strange, I tried the same but it is not triggering.
The script runs perfectly then I call the script as a service, or when changing housemode (state_trigger).
Is there anything else one needs to do to make this work? I have just make a script named “heatingschedule.py” inside the pyscript folder.

The input.datetime sensor is a “Time” only sensor, not “Date and time” sensor.

@state_trigger('input_select.housemode')
@time_trigger(f"once({input_datetime.heating_stop})")
def heatingcontrol_room():
    log.warning("Running script") #For testing right now
    heatingcontrol_setpoint("climate.heating_living_room", input_select.living_room_schedule)

Any packages being imported must be available in the HomeAssistant Core install
To install extra Python packages in HA, you can use Shell Commands:

shell_command:
  homeassistant_start: /config/shell_commands/homeassistant_start.sh

automation:
  - alias: "homeassistant_start"
    trigger:
      - platform: homeassistant
        event: start
    action:
      service: shell_command.homeassistant_start
#!/bin/sh

# /config/shell_commands/homeassistant_start.sh
# ----------------------------------------

pip install <your_package_here>
1 Like

How is this supposed to work when you change the value of input_datetime.normal_bedtime?

Surely the decorator is evaluated only once, on startup?

Yes, and therefore not a good example. Here is another one

@state_trigger("input_datetime.wakeuptime")
@time_trigger("startup")
def wakeuplight():
    global handle1
    global handle2

    @time_trigger(f"once({input_datetime.wakeuptime})")
    def wakeup():
        if binary_sensor.workday_sensor == "on" and binary_sensor.noen_er_hjemme == "on":
            script.morgenlys.turn_on()
            log.info('Morgenlys trigget')
    handle1 = wakeup

    @time_trigger(f"once({input_datetime.wakeuptime} -30m)")
    def effect():
        if binary_sensor.workday_sensor == "on" and binary_sensor.noen_er_hjemme == "on":
            script.soverom_effekt_morgen.turn_on()
            log.info('Effektlys trigget')
    handle2 = effect

3 Likes

Thank you very much, works perfect now! :smiley:

Totally newbie to this and after using the built in python service just to find out i couldnt find anything a community search sent me in the direction of pyscript.

I have installed via HACS and added the integration via the GUI but now is where i need some guidance.

Below is my pythin script that i use to change the in/out feeds on my HDMI matrix, it works as is when run from the command line using python3.

import telnetlib
import time
HOST = "192.168.1.10"
telnet = telnetlib.Telnet(HOST)
telnet.write(b'admin\r\n')
time.sleep(1)
telnet.write(b'admin\r\n')
time.sleep(1)
telnet.write(b'ss 07,03\r\n')

The question is what do i do with it to make it a switch within HA, i have copied it into the pyscripts folder but cant seem to find it under the service call function.

Thanks in advance for anyone kind enough to assist.

You’ll need to do a couple of things. First, you need to write your code as a function, perhaps with an @service decorator or some other triggers that describe how to call it, eg:

@service
def matrix_set():
    # put your code here

This creates a service called matrix_set. You can also add parameters - see the documentation,

Second, you cannot write any code that blocks (eg: telnet.write and time.sleep will block all other activity in HASS, which is a very bad thing). There are a couple of solutions:

  1. Use async versions of libraries that do I/O or block, eg: asynctelnet, and instead of time.sleep() use task.sleep(), which is a builtin function in pyscript, or:
  2. You can use task.executor() to run blocking functions in a separate thread. See the documentation and examples.

The first is the preferred solution, since the second adds quite a bit of overhead.

Thanks for that @craigb, as i said new to this so i appreciate you bearing with me on this.

I have taken your feedback on board and created the following matrix_set.py file, placed it in the folder /config/pyscript/ and restarted my HA.

@service
def matrix_set(input=None, output=None):
    import telnetlib
    import time
    HOST = "192.168.1.10"
    telnet = telnetlib.Telnet(HOST)
    telnet.write(b'admin\r\n')
    task.sleep(1)
    telnet.write(b'admin\r\n')
    task.sleep(1)
    telnet.write(b'ss input,output\r\n')

I can get the last line working when i specify the in / out number on the telnet write command but is there a way to pass variables ?

for example

pyscript.matrix_set(07,03)

Thanks

Andrew

Answering my own question, so this code hooks into the energy dashboard config and generates decorated functions as desired which in turn flatens out “outliers” on bad sensor readings.

from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.statistics import statistics_during_period

decorated_functions = {}

async def _get_statistic(
    start_time: datetime,
    end_time: datetime | None,
    statistic_ids: list[str] | None,
    period: Literal["5minute", "day", "hour", "week", "month"],
    types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]]):

    start_time = start_time.astimezone(timezone.utc)
    end_time = end_time.astimezone(timezone.utc)

    return(await get_instance(hass).async_add_executor_job(statistics_during_period, hass, start_time, end_time, statistic_ids, period, None, types))

@time_trigger("startup")
async def correct_bad_readings():
    global decorated_functions

    # fetch all sensors listed in the energy dashboard
    sensors = [k.get('stat_consumption') for k in hass.data['energy_manager'].data.get('device_consumption')]

    for sensor in sensors:
        log.debug(f"Corrector setting up {sensor}")
        @state_trigger(f"{sensor}")
        async def corrector(var_name=None, value=None):
            log.debug(f"Corrector checking {var_name}")

            start_time = datetime.now() - timedelta(minutes=30)
            end_time = datetime.now()

            stats = _get_statistic(start_time, end_time, [var_name], "5minute", 'state')
            unit = state.getattr(var_name).get('unit_of_measurement')

            stat = [{'start': d.get('start'), 'value': float(d.get('state'))} for d in stats.get(var_name)]
            previous, last = stat[:-1], stat[-1]
            previous_values = [v.get('value') for v in previous]

            average = sum(previous_values) / len(previous_values)
            delta = abs(last.get('value') - average)

            log.debug(f"Checking if delta ({delta}) is larger than avarage ({average})")

            if delta > average:
                log.debug(f"Delta to high for {var_name} | State: {last.get('value')}, Avarage value: {average}, Delta: {delta}")
                hass.data["recorder_instance"].async_adjust_statistics(statistic_id=var_name,
                                                                        start_time=last.get('start'),
                                                                        sum_adjustment=average,
                                                                        adjustment_unit=unit)

            decorated_functions[sensor] = corrector

    # this isn't needed afaik? or can we listen to an event regarding the energy manager, then this would be nice.
    for sensor in list(decorated_functions.keys()):
        if sensor not in sensors:
            del decorated_functions[sensor]

Now if I only could find which event (for event_trigger) fires save actions when adding/removing sensors in the energy dashboard :face_with_raised_eyebrow:

Eveing Alll … hopefully a simple one but how do you set a state of a binary sensor in pyscript ?

Tried this but doesnt seem to work

hass.states.set(binary_sensor.ps5_to_tv_state, 1)

Thanks

https://hacs-pyscript.readthedocs.io/en/stable/reference.html#state-variables

State variables can be accessed in any Python code simply by name. State variables (also called entity_id ) are of the form DOMAIN.name , where DOMAIN is typically the name of the component that sets that variable. You can set a state variable by assigning to it.

In the documentation:
state.set(name, value=None, new_attributes=None, **kwargs)

so

state.set('binary_sensor.ps5_to_tv_state', value='on', new_attributes=None, **kwargs)

and

binary_sensor.ps5_to_tv_state = 'on' would also work, but is less “safe”.

Really? This is how you’re going to join the community?

Fine fine.

Still, allowing all imports then not including basic file I/O sort of makes the whole thing redundant.