Pyscript - new integration for easy and powerful Python scripting

There’s an issue for VSCode support just opened here by @swiftlyfalling:

Unfortunately, I’ve never used VSCode. I did just try it, and got the same error as @swiftlyfalling. But as you can see from the issue, I made no progress trying to figure out how VSCode starts the Jupyter kernel.

@swiftlyfalling reports that you can paste the Jupyter server URI after starting it manually, eg:

http://127.0.0.1:8888/?token=long_hex_string

into VSCode (under URI of existing server) and it does work.

Did not see that, cool. I’ll look into it.
Have not used Jupyter before. Seems to be more experimenting in my future. =)
Thnx mate.

Hi, Newbie in programming (i’ve just learnt Python seriously during the first Lockdown…) i’m trying to apply what i’ve learnt to automate stuff with Python on HA.

I have tried AppDaemon, but due to the fact that i’m on Raspberry, i find Pyscript more convenient (i’m not excluding the fact that i may try it a little bit…)

I am trying to make a script which will notify me when an action is available or not.
I’ve well understood the fact that the decorater @state_trigger can send the disctionnary that can be passed as an argument through the associate function.

But is there a way to get those arguments (especially “old_value”) with a get.state function ?

@state_trigger("input_boolean.pyscript")
def essai():
    log.info(f"{sensor.my_sensor}") #I want for example the old value of this sensor but sensor.my_sensor.old_value doesn't seems to work

Thanks

pyscript does not keep all the historical values of an entity. Even Native Home Assistant Automations don’t have access to this data by default. But, there’s an easy way to do what you want with pyscript:

OLDVALUE = None

@state_trigger("input_boolean.pyscript")
def essai():
  global OLDVALUE

  log.info(f"The old value is {OLDVALUE}. The new value is {sensor.my_sensor}.")

  OLDVALUE = sensor.my_sensor

You can, of course, expand this to keep track of more than one entity, entity attributes, or whatever else you might need.

The one place the old value is available is inside the trigger decorators, and that particular state variable just changed. The old value is also passed into the trigger function using an optional keyword argument old_value. So your function can get the old value when the trigger is caused by a change in that state variable:

@state_trigger("input_boolean.pyscript")
def essai(var_name=None, value=None, old_value=None):
    log.info(f"essai triggered: var_name={var_name}, new value={value}, old value={old_value}")

The old_value might be None on the very first trigger, eg: if the state variable didn’t previously exist.

Thanks for both answers,

@craigb: Your example take the case when the function is triggered by the @state_trigger

But i’m looking to have the old_value of another sensor inside the function

@state_trigger("input_boolean.pyscript") #Input boolean trigger 
def essai():
    log.info(f"{sensor.wallplug_fibaro}") #I want to get the old value of the fibaro for example (Here is gonna be off or on obviously)

I have the Variable custom component so i may need to create old_value as a variable and store it like this ?

At this point I’m a few hours into this, I was excited when I saw pyscript, exactly what I needed, but I can’t get simple things to work… All I want to do is open a file and pass that binary data to a local API running on my network. This is incredibly difficult, the python “open” will not work.

I have Allow All Imports on.

NameError: name 'open' is not defined

  • I’ve tried this directly in my pyscript and it doesn’t work.
  • I’ve created a module and tried to “open” the file there and imported that module into my myscript and it won’t work.
  • I’ve created a “/config/pyscript/modules/MYAPP” and it didn’t work.
  • I’ve tried resp = task.executor(mymodule.test, "/the/file/name") and it doesn’t work. My code in the function “test” never runs. What do I do with “resp”? It’s nowhere to be found in the documentation.

Am I doing something radical here that’s not supported?

What am I doing wrong?

Not supporting I/O like open is intended to avoid security issues, and also avoid the problem of doing I/O in the main event loop. But I agree that if you set allow_all_imports then there’s a good case to have open and file I/O work too. Let me consider that.

In the meantime, I’d recommend putting your I/O code in a native python module, which doesn’t go below the pyscript directory. If you look here in the docs, you can set python’s path to allow imports of native python code from another directory, eg:

import sys

if "config/pyscript_module" not in sys.path:
    sys.path.append("config/pyscript_modules")

You can then put a module in config/pyscript_modules that opens and reads the file(s) you need, and returns the contents.

Yes, you should use task.executor from pyscript when you call that function, since I/O shouldn’t be done in the event loop.

2 Likes

wow, you are awesome craigb such a fast response.

That worked.

One other question is, task.executor has a return object. How do I use it? I can’t find any examples so I’m not sure how to use the response.

Thanks again, this was very helpful.

Update: Nevermind, I got that figured out too. It’s just the response you return from the method being invoked. Thanks!

I updated the docs to explain that open, read and write are missing, and suggestions about how to do file I/O.

1 Like

I’m not sure if you noticed, but pyscript version 1.0.0 works with a VSCode client. The pyscript kernel should look like a regular Jupyter kernel that VSCode can connect to. You’ll also need to install the latest hass-pyscript-jupyter version 1.0.0, which is now available via PyPi.

1 Like

Thanks for that.
Apparently, I am a pleb and don’t have Jupyter. :grin:
I should probably get on that, may make my dev experience a little easier. My env is my laptop, but almost everything runs from my server which multi-tasks as just about everything a normie would need a personal server for at home :sweat_smile:

Does Pyscript support folders inside the pyscript folder?

I’d like to be able to group my automations by what they do (thermostat/light/alerts) rather than have a giant mess in one folder.

No, not yet. Currently only the sub-directories apps and modules allow scripts below the top-level directory, but those have specific purposes, and scripts in modules are not auto loaded, and only the first-level .py files and the __init__.py files one more level down in apps are autoloaded.

A couple of other folks have requested that too. It sounds like a good idea, and I will plan on adding it to the next version. It’s actually a pretty easy thing to add.

Questions: should pyscript automatically load scripts recursively in all other directories (other the special directories apps and modules)? Or should we reserve a name like local, and everything below just that directory is recursively auto loaded? Should the directories that are recursively autoloaded be configurable?

My leaning would be to have one directory, eg, local, which has recursively autoloading, and not make its name configurable. My reasoning is that we might add other top-level directories that support special behavior, so keeping the recursive loading to one directory would avoid future name collisions. For config, I prefer to keep things as simple as possible, but we could always make it configurable later (eg, a list of directories to recursively autoload, and the default is local).

Thoughts?

Having /local that then recursed and loaded everything under it would work.

The option to set the directories would be nice as well but not needed.

Thank you for this custom component @craigb. Well done.

One question, I do have a python script where I import sys and dropbox to upload snapshots of my security cameras to Dropbox. Is that something pyscript can handle?

I’m not @craigb, but, the answer is “probably”.

with “allow_all_imports: True” in your pyscript configuration, you’ll have no issue importing sys and dropbox. Using those libraries, however, will depend on how they work. A few extra hoops have to be jumped through to do Blocking I/O tasks. It’s not a limitation of pyscript, but a limitation of the fact that it is running INSIDE of Home Assistant and therefore, has to play nice with all of the other things Home Assistant is doing at the time.

Specifically, any Blocking I/O task will need to be performed using Pyscripts task.executor functionality, and, depending on how the library works, may need to be broken off into a smaller companion module to be imported.

The best approach would be to write a few lines of code that use these libraries in the way you intend to use them, and then post here for confirmation or assistance if it doesn’t work like you’d expect. Once you get the interaction with those libraries ironed out, the rest is easy.

Thanks @swiftlyfalling for your answer and happy new year! I tried your idea but my script would work and throw errors with my imports. Not sure whether I should give up or give it another try :slight_smile:

if you share the script you’re trying to run, I can probably help figure out why you’re getting issues.

That’s kind of you @swiftlyfalling. I’ve added the script below.

The script already fails at the import lines. I have uploaded the whole /Dropbox package to /pyscript/modules/Dropbox but it didn’t help.

Python code
@service

import dropbox
from dropbox.files import WriteMode
from dropbox.exceptions import ApiError, AuthError

TOKEN = 'TOKEN'

LOCALFILE = 'IMG_3179.png'

# Uploads contents of LOCALFILE to Dropbox
def backup():
    with open(LOCALFILE, 'rb') as f:
        # We use WriteMode=overwrite to make sure that the settings in the file
        # are changed on upload
        try:
            dbx.files_upload(f.read(), '/camera_snapshot.png', mode=WriteMode('overwrite'))
        except ApiError as err:
            # This checks for the specific error where a user doesn't have
            # enough Dropbox space quota to upload this file
            if (err.error.is_path() and
                    err.error.get_path().reason.is_insufficient_space()):
                sys.exit("ERROR: Cannot back up. Insufficient space.")
            elif err.user_message_text:
                print(err.user_message_text)
                sys.exit()
            else:
                print(err)
                sys.exit()

    
def create_link():
    result = dbx.sharing_create_shared_link(BACKUPPATH, short_url=False, pending_upload=None)
    print("File is available at " + result.url)


if __name__ == '__main__':
    # Check for an access token
    if (len(TOKEN) == 0):
        sys.exit("ERROR: Looks like you didn't add your access token.")

    # Create an instance of a Dropbox class, which can make requests to the API.
    print("Creating a Dropbox object...")
    with dropbox.Dropbox(TOKEN) as dbx:

        # Check that the access token is valid
        try:
            dbx.users_get_current_account()
        except AuthError:
            sys.exit("ERROR: Invalid access token")

        # Create a backup of the current settings file
        backup()

        # Change the user's file, create another backup
        create_link()

The code does work but I am facing issues “adapting” it to work with pyscript and home assistant.