PyScript version 1.0.0 Released!

PyScript 1.0.0 has been released!

If you’re not already using PyScript, written by @craigb, you’re in for a treat. If you are, skip down a bit to see some highlights of the new features in this release.

What is PyScript?

PyScript is an alternative implementation for Home Assistant Automations. Similar in functionality to Node-RED’s Home Assistant integration or AppDaemon, it allows you to write Home Assistant Automations in what will be, for some, a much simpler syntax.

As an example, in Home Assistant, you might want to turn on a light for 5 minutes anytime a motion detector senses motion and it’s dark outside. That automation might look like this:

- alias: porch lights on
  trigger:
    - platform: template
      value_template: >-
        {{
            is_state('binary_sensor.motion', 'on')
            and is_state('binary_sensor.dark', 'on')
        }}
  action:
    service: light.turn_on
    data:
      entity_id: light.porch
      brightness: 255
      color_temp: 300

- alias: porch lights off
  trigger:
    - platform: state
      entity_id: binary_sensor.motion
      to: "off"
      for: 300
  action:
    - service: light.turn_off
      data:
        entity_id: light.porch

In PyScript, that automation might look like this:

@state_trigger('binary_sensor.motion == "on" and binary_sensor.dark == "off"')
def porch_lights_on():
  light.porch.turn_on(brightness=255, color_temp=300)

@state_trigger('binary_sensor.motion == "off"', state_hold=300)
def porch_lights_off():
  light.porch.turn_off()

PyScript offers many more features all designed to keep automations easy to read, even when the actions they perform are complex. If you’ve ever written a Home Assistant Automation with the choose action in it, you’ll likely really appreciate the simpler form for this kind of logic in PyScript.

PyScript lets you use Python, instead of YAML, to write your automations. And with this comes nearly all of the Python language features. Python is incredibly simple to write – certainly not any more difficult than Home Assistant Automation YAML – and is designed to express logic like this.


New Features in 1.0.0

In this release there are a couple of very notable features.

Short Form Service Calls

Before this release, turning on a light looked like this:

light.turn_on(entity_id='light.porch')

Now, while that syntax still works, you can instead do this:

light.porch.turn_on()

Any service call that requires an entity_id can be shortened in this way. While, in a single line like this, it make not look like much, it does a lot to improve the readability of your automation, and it requires fewer keystrokes.

State Persistence

There are times when an automation works best if we can store some piece of data and retrieve it later and we want that information to be maintained even if we need to restart Home Assistant.

Perhaps, for some reason, you need to keep track of how many times a switch has turned on. In Home Assistant automations, you’d likely use a counter helper, or maybe an input_number.

First, you have to create that helper. Once created, you might write an automation like this:

- alias: count my thing
  trigger:
    - platform: state
      entity_id: switch.my_thing
      to: "on"
  action:
    - service: counter.increment
      data:
        entity_id: counter.my_thing

In PyScript, you don’t need the helper at all. Instead that automation might look like this:

state.persist('pyscript.switch_counter', default_value="0")

@state_trigger('switch.my_thing == "on"')
def count_my_thing():
  pyscript.switch_counter = int(pyscript.switch_counter) + 1

The state.persist call lets PyScript know that you want this state to be persisted. After that, you can just update it like any other variable. If you don’t need the data restored after a Home Assistant restart, you can leave the state.persist call out entirely and the rest will still work as you’d expect.

Working with Attributes

A lot of times, in Home Assistant Automations the solution to working with an attribute is to write a Template Sensor to expose that attribute in its own entity_id and then write your Automation against this new entity_id. With PyScript, that is, of course, still an option. But, in this release, all @state_triggers and task.wait_until() calls can use attributes directly.

Perhaps you want to get a notification if your thermostat is set higher than 80 degrees:

@state_trigger('climate.thermostat.temperature > 80')
def send_thermostat_notification():
  notify.me(
      message=f"The Thermostat is now set to {climate.thermostat.temperature}, which is above 80"
  )

Using task.wait_until() you can also get a notification when the thermostat setpoint is restored to a more reasonable setting:

@state_trigger('climate.thermostat.temperature > 80')
def send_thermostat_notification():
  task.unique('thermostat_notification')
  notify.me(
      message=f"The Thermostat is now set to {climate.thermostat.temperature}, which is above 80"
  )
  task.wait_until(state_trigger="climate.thermostat.temperature <= 80")
  notify.me(
      message=f"The Thermostat is now set below 80 degrees"
  )

state_hold on @state_trigger

The state_hold kwarg on a @state_trigger works similar to the for: option in a Home Assistant Automation Trigger. Before this feature, you had to jump through a few extra hoops with task.unique() and task.sleep() in order to wait for an entity to be in a certain state for a certain amount of time. But that’s no longer the case.

Logbook logging

When Home Assistant Automations fire, if you use the Logbook, you can consult it to see which automation turned on what. This helps answer the question “Why is this light on?” when Automations don’t work exactly like we thought they would.

With this release, you’ll see PyScript automations appearing in the logbook too, giving you a record of which PyScript turned on which light and when.

VSCode Jupyter Kernel

PyScript has an optional Jupyter Notebook kernel you can use. This provides entity_id completion and also allows you to test automations without having to reload PyScript after every change. Once you have it working the way you want, you copy and paste the code into the PyScript directory and you’re done.

In this release, PyScript’s Jupyter Kernel works with VSCode’s Jupyter Notebook implementation. This means if you’re using VSCode to edit your Automations and PyScript files, you don’t have to leave that environment to test out some new code using Jupyter Notebook.

Much More

I’ve only highlighted a few of the new features available.

Check out the full release notes to get the details on all the changes in this release.

Share!

Share in the comments one of your PyScript automations to help others get a feel for just how easy automations can be with PyScript.

9 Likes

Wish I could upvote this more than once! The enhancements and improvements are fantastic. Thank you to all who have contributed to PyScript’s rapid evolution.

In my opinion, PyScript has quickly grown to be far more capable than the existing python_script integration (and easier to use) that it merits superseding it as Home Assistant’s officially supported “python script” integration.

FWIW, I really like the new compact syntax:

  light.porch.turn_on(brightness=255, color_temp=300)
1 Like

I agree. I never got much mileage out of python_script. Even when dodging around the more complicated syntax, I still had to have an “automation” to get anywhere with it since they can only be used as services.

The syntax in pyscript is MUCH nicer. And, they can be triggered without also needing a Home Assistant automation, so that works out really well.

1 Like

Is there much (any) overhead compared to the core automation method?

I know very little about python but the examples you have shown are very tempting.

I can’t imagine there is much. When it all boils down, regular native Home Assistant Automations are implemented in much the same way using the same state and event API. There may be some preferential treatment to run native Automations First (I don’t know, I haven’t dug through all that code), but otherwise, you should be unable to tell in operation if it was pyscript or native.

The other alternative automation platforms mentioned (Node-RED and AppDaemon) use the websocket API. Even then, they are plenty fast for most use cases. But PyScript should be even faster than those since there’s no websocket to deal with and the API it uses is the same one used by Native Automations.

3 Likes

Hi
I just tried iPyScript( installed it through HACS) on HomeAssistant 0.118.0).
Installation runs smoothly and I’ve added a custom py script to the config folder.
But after multiple restarts the pyscript service is not showing up in the services tab.
I can not find any issues in the logs.
In addition my custom py script ( pyscript.myscript_ ) does not show up.

Any idea what could fix this ?

Thanks

You need to either add the integration via the Integrations UI or include “pyscript:” in your configuration.yaml.

I’ve found categories like python_script, appdaemon, … but NO PYSCRIPT.
Wouldn’t it be nice to have a separate category for PYSCRIPT? with subcategories?
I like pyscript very much. There’s the separate https://hacs-pyscript.readthedocs.io/en/latest/index.html
but, no community around it. The pyscript code is here and there on this site. Not easy to find. (at least, not for me)
A place, dedicated to pyscript users, would be so nice.
and… I hope one day, pyscript can be added to Core. It’s too important in my view.

I agree, pyscript should be a category in these forums. I’ve contacted a maintainer about adding it, but it hasn’t happened yet.

Github offers a discussions feature. Perhaps that could be turned on? @craigb

I just reached out to an admin for the community forums to request a subcategory for pyscript. Let’s see if that succeeds.

Otherwise I’ll look at the GitHub alternative.

2 Likes

thanks! Looking forward to it :slight_smile:

Hi guys, I have a script that depends on these modules, will it work with PyScript?

import sys
import socket
import binascii
import libscrc
import json
import os

I can’t answer that personally, however if you’re using sockets, then you should just create an integration.

In short, any module will work as long as you install it (requirements.txt, in most cases) and are able to separate pyscript functionality into separate methods when needed to utilize the python modules correctly.

I disagree with @petro in this case. While you CAN make an integration instead, it’s quite a bit more work in a lot of cases (because of the Home Assistant Boilerplate required to get started). So, often, at least for me, using pyscript I can get the job done more quickly, with less code that is easier to decipher and debug, than I can with a full Home Assistant integration.

If you do choose to go the integration route, the custom-component/integration_blueprint will help ensure you get all that boilerplate stuff in line.

Based on what I’ve seen from pyscript, I don’t see how a socket can be properly used without all that overhead you’re saying that he won’t need. :man_shrugging:

It really depends on exactly what they are doing with it. If they are just sucking in events and relaying some of that information as binary_/sensor entities, I imagine it could easily fit in 100 lines of code (which is just 1 of the 9+ files the linked repo provides as boilerplate).

Even if they were equal in complexity and length, I find the process of writing a pyscript to be quite a bit easier than writing a full integration. Auto Reloading on changes and no restart of Home Assistant really speeds things along (though, maybe Home Assistant can do that now during integration development, it’s been 6+ months since I’ve written one).

I honestly don’t know why it’s so complex. I thought about writing an integration that handled all the boilerplate stuff with only a configuration file, providing classes and functions to remove some of the complexity, but then I realized I’d basically just be writing pyscript.

It can’t do everything, but it can sure do a lot. I’ve implemented all sorts of things from “wasp in a box” sensors (that someone really should make an integration for that works well, because they are so useful), to minecraft server sensors and notifications, to classes that simplify acting on zwave events. It’s really quite capable and speedy, and all inside of home assistant.

I can share the code, maybe you guys can think of a better solution:

#!/usr/bin/python3
# Script gathering product data from Sofar Solar Inverter (K-TLX) via WiFi logger module LSW-3
# by Michalux

import sys
import socket
import binascii
import libscrc
import json

def padhex(s):
    return '0x' + s[2:].zfill(4)

def hex_zfill(intval):
    hexvalue=hex(intval)
    return '0x' + str(hexvalue)[2:].zfill(4)

inverter_ip=str('192.168.0.155')
inverter_port=int(8899)
inverter_sn=int(2788986774)
lang=str('EN')
verbose=str('1')
# END CONFIG

# PREPARE & SEND DATA TO INVERTER via LOGGER MODULE
output="{" # initialise json output

pini=33031 # START REGISTER
pfin=33032 # END REGISTER

SN="\""
SV="\""
HV="\""
DSPV="\""

# Data logger frame begin
start = binascii.unhexlify('A5') # Logger Start code
length=binascii.unhexlify('1700') # Logger frame DataLength
controlcode= binascii.unhexlify('1045') # Logger ControlCode
serial=binascii.unhexlify('0000') # Serial
datafield = binascii.unhexlify('020000000000000000000000000000') # com.igen.localmode.dy.instruction.send.SendDataField
# Modbus request begin
pos_ini=str(hex_zfill(pini)[2:])
pos_fin=str(hex_zfill(pfin-pini+1)[2:])
businessfield= binascii.unhexlify('0104' + pos_ini + pos_fin) # Modbus data to count crc
if verbose=="1": print('Modbus request: 0104 ' + pos_ini + " " + pos_fin +" "+str(padhex(hex(libscrc.modbus(businessfield)))[4:6])+str(padhex(hex(libscrc.modbus(businessfield)))[2:4]))
crc=binascii.unhexlify(str(padhex(hex(libscrc.modbus(businessfield)))[4:6])+str(padhex(hex(libscrc.modbus(businessfield)))[2:4])) # CRC16modbus
# Modbus request end
checksum=binascii.unhexlify('00') #checksum F2
endCode = binascii.unhexlify('15')# Logger End code
inverter_sn2 = bytearray.fromhex(hex(inverter_sn)[8:10] + hex(inverter_sn)[6:8] + hex(inverter_sn)[4:6] + hex(inverter_sn)[2:4])
frame = bytearray(start + length + controlcode + serial + inverter_sn2 + datafield + businessfield + crc + checksum + endCode)
if verbose=="1":
    print("Hex string to send: A5 1700 1045 0000 " + hex(inverter_sn)[8:10] + hex(inverter_sn)[6:8] + hex(inverter_sn)[4:6] + hex(inverter_sn)[2:4] + " 020000000000000000000000000000 " + "0104" + pos_ini + pos_fin + str(hex(libscrc.modbus(businessfield))[3:5]) + str(hex(libscrc.modbus(businessfield))[2:3].zfill(2)) + " 00 15")
if verbose=="1": print("Data sent: ", frame);
# Data logger frame end

checksum = 0
frame_bytes = bytearray(frame)
for i in range(1, len(frame_bytes) - 2, 1):
    checksum += frame_bytes[i] & 255
frame_bytes[len(frame_bytes) - 2] = int((checksum & 255))

# OPEN SOCKET
for res in socket.getaddrinfo(inverter_ip, inverter_port, socket.AF_INET, socket.SOCK_STREAM):
                 family, socktype, proto, canonname, sockadress = res
                 try:
                  clientSocket= socket.socket(family,socktype,proto);
                  clientSocket.settimeout(15);
                  clientSocket.connect(sockadress);
                 except socket.error as msg:
                  print("Could not open socket - inverter/logger turned off");
                  if prometheus=="1": prometheus_file.close();
                  sys.exit(1)

# SEND DATA
clientSocket.sendall(frame_bytes);

ok=False;
while (not ok):
 try:
  data = clientSocket.recv(1024);
  ok=True
  try:
   data
  except:
   print("No data - Exit")
   sys.exit(1) #Exit, no data
 except socket.timeout as msg:
  print("Connection timeout - inverter and/or gateway is off");
  sys.exit(1) #Exit

# PARSE RESPONSE (start position 56, end position 60)
if verbose=="1": print("Data received: ", data);
i=pfin-pini # Number of registers
a=0 # Loop counter
response=str(''.join(hex(ord(chr(x)))[2:].zfill(2) for x in bytearray(data))) #+'  '+re.sub('[^\x20-\x7f]', '', '')));
if verbose=="1":
    hexstr=str(' '.join(hex(ord(chr(x)))[2:].zfill(2) for x in bytearray(data)))
    print("Hex string received:",hexstr.upper())

while a<=i:
 p1=56+(a*4)
 p2=60+(a*4)
 responsereg=response[p1:p2]
 val1=chr(int(str(responsereg[0:2]),16))
 val2=chr(int(str(responsereg[2:4]),16))
 hexpos=str("0x") + str(hex(a+pini)[2:].zfill(4)).upper()
 if verbose=="1": print("Register:",hexpos+" ("+str(int(hexpos,16))+"), Value: "+str("0x"+responsereg)+" ("+str(int(str(responsereg),16))+")");
 a+=1
    output=output+"\"Serial Number" + "\":" + SN +"\","
    output=output+"\"Software Version" + "\":" + SV +"\","
    output=output+"\"Hardware Version" + "\":" + HV +"\","
    output=output+"\"DSP Version" + "\":" + DSPV +"\","
    output=output[:-1]+"}"
jsonoutput=json.loads(output)
print(json.dumps(jsonoutput, indent=4, sort_keys=False, ensure_ascii=False))

It’s really not that complex if you understand object oriented programming. HA does everything for you except the background data, you just need to know the API calls. So, you’d just make a background data object and pass the information when you want the sensor to update. You don’t even need to build a configuration, you can just hardcode everything which skips %40 of integration coding with something that small. Lastly, you don’t have a middle man, you get straight to your data.

Again, I’m not saying pyscript is a POS. I’m simply pointing out that this is a situation where you’d want to create an integration over a simple automation. A good rule of thumb: Does it create sensors? Does it require constant connection instead of spooled up and destroyed connections (Sockets)? If yes to both of those, create an integration. I’d wager that it’s the same amount of code or less and you’ll have a more robust system.

I don’t understand why that code is building JSON manually, only to convert it to a python object, and then convert it back to JSON?

Regardless of if you write a pyscript or an integration, you’ll need to handle all of the exits differently.

If you’ll only be executing that chunk of code at some interval, you can save yourself to trouble of having to convert it to anything at all by running it as a command line sensor.

If you decide to use an integration, you won’t need binary_sensor or switch. And you can either use the config entry stuff and get rid of the configuration at the top, or get rid of the config entry stuff entirely and leave the configuration stuff in the code.

If you decide to go with pyscript, which I believe this would work just fine in, you’ll basically need to wrap all of this in a function, get rid of the exits in favor of return, and then decorate the function with a @time_trigger indicating how often you want it to run. You’ll also need to replace the print parts with something that actually creates the sensor (i.e. state.set(something, something)).

For me, personally, I’d write this in pyscript. For something the doesn’t require entities other than sensor. or binary_sensor. it’ll be less work, in my opinion. If you want to create entities in other domains, pyscript is not an option at the moment, though I have a PR open that paves the way for other entity types.