Pyscript - new integration for easy and powerful Python scripting

I would try doing something like below. Basically, you need to convert the string value to a date value. I don’t remember off the top of my head what the date format is that HA uses, but you can output that and then use the format strings (datetime — Basic date and time types — Python 3.12.2 documentation) to get it to parse correctly. This should at least get you going in the right direction.

last_flash = datetime.strptime(input_datetime.last_office_flash, [date format])
log.info(dt.now() - last_flash).total_seconds

All states come in as strings. I don’t know when the last time that pyscript was updated though.

1 Like

Ugh, thanks. It should have been obvious that everything would come in as a string but the “StateVal” was throwing me off.

1 Like

The last couple of days pyscript has stopped reacting to state changes for me. For example, I have this very simple trigger:

@state_trigger("sensor.kontor_bordsfjarr_action == 'single'")
def kontor_bordsfjarr(**kwargs):
    light.toggle(entity_id="light.kontor_taklampa")

I can see it getting initialized on startup:

2024-04-02 08:48:30.235 DEBUG (MainThread) [custom_components.pyscript.trigger] trigger file.kontor.kontor_bordsfjarr: watching vars {'sensor.kontor_bordsfjarr_action'}
2024-04-02 08:48:30.235 DEBUG (MainThread) [custom_components.pyscript.trigger] trigger file.kontor.kontor_bordsfjarr waiting for state change or event

And I can see the sensor entity change in Developer Tools, but pyscript is not reacting to it. My pyscript apps execute like normal upon startup (some of them have an empty @time_trigger decorator and I can see the results).

Anyone has any tips on how I can troubleshoot this?

Hi, did you manage to solve this? :slight_smile:

No. I gave up and took a different approach to solve my issue.

As part of my View Assist project I am trying to implement some disposable timers that can be called using Assist voice assistant. I am doing this to prevent from having a lot of timer helpers for each satellite device.

I think I will be able to do this with Pyscript as the simple proof of concept I have put together works:

import asyncio
@service
def viewassist_timer(action=None, duration=None, id=None):
    if action == "starttimer" and duration is not None:
        time_remaining = duration
        while time_remaining >0:
            asyncio.sleep(1)
            time_remaining -=1
        event.fire('viewassist_timer_expired', param1=action, pararm2=id, param3=duration) 

This simple script works in that the user can say “Set an egg timer for 120 seconds” and a service call will be made to this function that waits for the duration and then returns an event that can then be used to alert the user that the timer has expired.

This works but I’d like to add something in the while loop that would detect if the user wanted to cancel the timer. Can I put an event trigger within that loop? I’m thinking if I had an event that contained the event_type ‘viewassist_timer_cancel’ along with either the id (for example “egg”) or the duration then that could identify the timer to cancel. I am also wanting to have multiple timers running simultaneously and would need a way to separate the running processes.

import asyncio

@service
def viewassist_timer(action=None, duration=None, id=None):
    cancel = "false"
    if action == "starttimer" and duration is not None:
        time_remaining = duration
        #while (time_remaining >0):
        while (time_remaining >0) and (cancel != "true"):
            asyncio.sleep(1)
            time_remaining -=1
        if time_remaining == 0:
            event.fire('viewassist_timer_expired', action=action, id=id, duration=duration)
        else:
            event.fire('viewassist_timer_canceled', action=action, id=id, duration=duration)
    if action == "canceltimer":         
        cancel = "true"

I tried doing this ^^ in the hopes that I could use the service to cancel the timer but I think because Pyscript is spawning a new instance for each timer service call made that I can’t get back to canceling the running one.

As I am learning more, I am seeing that Pyscript can create devices. Can I use it to create timers themselves and have them go away after a time? This tool seems very powerful.

Anyone have thoughts on how to best do this?

There’s two ways I could see how that may work. The first one would be having the timers in a separate python fuction that is called by the viewassist_timer function. If pyscript supports asyncio tasks, you could make a dict at the start of the python file that the functions can access (may need to make the dict global there?). The keys of the dict could be the event id, and the value for said key would be an asyncio task if a timer is running, that you would cancel if the cancel event is fired. I believe it’s possible to run the same async function multiple times under different tasks.

In case the dict version doesn’t work (i.e. you can’t access updated data of it due to is spawning new instances), you could also try making a single sensor in pyscript, with the attributes being which timers are active or what the states of the timers are. Upon needing to cancel you can retrieve the attributes of this sensor and see if it is active. Though you’d still need a way to retrieve the actual tasks running the timer, which means you’d still need to access the dict.

Upon building the example code, I did find the option to set task names, however to get a task using the name, you’d have to use asyncio.all_tasks() and I’m not sure if that will yield all asyncio tasks running in Home Assistant, which I assume will not be ideal.
Anyhow, I haven’t tried this code out on my own instance, but I think it could look something like this:

import asyncio

timer_dict = {}

async def async_timer(action=None, duration=int, id=None):   
    await asyncio.sleep(duration)
    event.fire('viewassist_timer_expired', action=action, id=id, duration=duration)

@service
def viewassist_timer(action=None, duration=None, id=None):
    timer_task = timer_dict.get(id, None)
    if action == "starttimer" and duration is not None:
        ##Cancelling the timer task if it is already running, in order to restart this 
        ##(Remove this line if restarting isn't desired) and put the create tasks in the if statement if the task is none
        if timer_task != None:
            timer_task.cancel()

        timer_task = asyncio.create_task(async_timer(action=action, duration=duration, id=id))
        timer_dict[id] = timer_task

        #Since the function is not async right now, this throws an error, however I am not sure how pyscript handles this within its own context
        #await timer_task 
        
    elif action == "canceltimer":         
        if timer_task != None:
            timer_task.cancel()

        event.fire('viewassist_timer_canceled', action=action, id=id, duration=duration)
        timer_dict[id] = None

I am not an expert on asyncio in any way, and have never used it in my python scripts explicitly, as far as I can remember. I usually get my async tasks working by simply running the code, seeing if it works, changing the code, and running it again. Rinse and repeat.

Also, I haven’t managed to have pyscript make devices, and not for a lack of trying. I’m afraid I can’t help you on that front, but would love to hear how to if you manage to figure it out!

EDIT:
Another idea I had on how you could possibly make this work: make a sensor with the ID’s as attributes, where each attribute’s value would be a timestamp pointing to when it should finish. Then use a pyscript automation that triggers when the time is equal to one of the attributes. You’d have to play around however with how to get the ID of which timer finished, as well as how to get the triggering working correctly, since the triggers will be dynamic of course.

1 Like

Just discovered Pyscript and loving it!

Full disclosure :slightly_smiling_face: been looking for some time to program something in Python. This is a good excuse. Ex C++ & full stack web dev

1 Like

I’m trying to debug an issue with my garden light not coming on. How can I ensure this constantly triggers every week. I have

        garden_time_on_weekday = "sunrise"
        garden_time_off_weekday = "sunset + 3 hours"

        garden_time_on_weekend = "sunrise + 1 hour"
        garden_time_off_weekend = "sunset + 3 hours"

        @time_trigger(f"once(monday {garden_time_on_weekday})")
        @time_trigger(f"once(tuesday {garden_time_on_weekday})")
        @time_trigger(f"once(wednesday {garden_time_on_weekday})")
        @time_trigger(f"once(thursday {garden_time_on_weekday})")
        @time_trigger(f"once(friday {garden_time_on_weekday})")
        @time_trigger(f"once(saturday {garden_time_on_weekend})")
        @time_trigger(f"once(sunday {garden_time_on_weekend})")
        def turn_on_garden():
            ...

Which I thought would trigger every week but it seems like it triggers once on each day for a week, then stops working. How can I create a trigger like I intended? Will just putting @time_trigger(f"once("sunrise") and sleeping work? Or will I have the same problem?

Is it possible to create a functional timer device using HACS Pyscript? I have used state.set and state.setattr to create what looks like a timer device. This device even shows the ‘start’ button for the timer but when clicked the timer does not start.

Am I trying something that is not possible or is there something more than setting the state to ‘idle’ and the duration as ‘0:00:30’ or similar?

Me again. Trying something different:

import wikipedia
@service
def search_wikipedia(searchterm=None, return_response=True):
  summary = wikipedia.summary(searchterm, sentences = 2) 
  response_variable = { "summary": summary }
  return response_variable

and a service call like this:

service: pyscript.search_wikipedia
data:
  searchterm: "Taylor Swift"

but this gives this error:

This error originated from a custom integration.

Logger: custom_components.pyscript.file.wiki.search_wikipedia
Source: custom_components/pyscript/eval.py:493
integration: Pyscript Python scripting (documentation, issues)
First occurred: 9:08:32 PM (3 occurrences)
Last logged: 9:09:14 PM

Exception in <file.wiki.search_wikipedia> line 4: summary = wikipedia.summary(searchterm, sentences = 2) ^ RuntimeError: Blocking calls must be done in the executor or a separate thread; Use `await hass.async_add_executor_job()`; at custom_components/pyscript/eval.py, line 1941: return func(*args, **kwargs) (offender: /usr/local/lib/python3.12/site-packages/urllib3/connection.py, line 219: return _HTTPConnection.putrequest(self, method, url, *args, **kwargs))

I don’t know much so could use some guidance on why this is not working. Please.

I think that has to do with the I/O blocking: Reference — hacs-pyscript 1.5.0 documentation
Which could be the case since the wikipedia module is likely making network requests.

According to the docs, task.executor can be used, but I am not 100% sure if that also returns a value. Can you try if this works?

import wikipedia
@service
def search_wikipedia(searchterm=None, return_response=True):
  #summary = wikipedia.summary(searchterm, sentences = 2) 
  summary = task.executor(wikipedia.summary, searchterms, {"sentences": 2})
  response_variable = { "summary": summary }
  return response_variable

(Syntax should be correct but am not 100% sure)

Thanks for this for sure. I gave it a go and it’s closer:

This error originated from a custom integration.

Logger: custom_components.pyscript.file.wiki.search_wikipedia
Source: custom_components/pyscript/eval.py:493
integration: Pyscript Python scripting (documentation, issues)
First occurred: 3:46:33 PM (4 occurrences)
Last logged: 3:48:04 PM

Exception in <file.wiki.search_wikipedia> line 5: summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2}) ^ PageError: Page id "taylor shift" does not match any pages. Try another id!

Notice that it is saying Page id “taylor shift” does not match any pages. I double checked my settings and I did not type the last name wrong:

service: pyscript.search_wikipedia
data:
  searchterm: "Taylor Swift"

I also added the double quotes to see if it helped but it didn’t.

I did change the variable you have set as ‘searchterms’ to ‘searchterm’ as defined in the function parameters. I do not think that has an impact though right?

Something definitely odd:

service: pyscript.search_wikipedia
data:
  searchterm: "Madonna"

Tried searching for something that had a one word title. I still get taylor shift not found.

This error originated from a custom integration.

Logger: custom_components.pyscript.file.wiki.search_wikipedia
Source: custom_components/pyscript/eval.py:493
integration: Pyscript Python scripting (documentation, issues)
First occurred: 3:46:33 PM (6 occurrences)
Last logged: 3:56:31 PM

Exception in <file.wiki.search_wikipedia> line 5: summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2}) ^ PageError: Page id "taylor shift" does not match any pages. Try another id!
Exception in <file.wiki.search_wikipedia> line 5: summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2}) ^ KeyError: 'query'

EDIT

A restart of HA returns a different error when using Madonna search term:

This error originated from a custom integration.

Logger: custom_components.pyscript.file.wiki.search_wikipedia
Source: custom_components/pyscript/eval.py:493
integration: Pyscript Python scripting (documentation, issues)
First occurred: 3:59:14 PM (2 occurrences)
Last logged: 3:59:30 PM

Exception in <file.wiki.search_wikipedia> line 5: summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2}) ^ KeyError: 'query'

MORE EDITING:

So the problem is that I need to add an additional parameter to turn off auto_suggest:

import sys
import wikipedia
search = "Taylor Swift"
result = wikipedia.summary(search, sentences = 2, auto_suggest=False)
print (result)

This, in regular python, will produce the results I expect. I tried modifying the pyscript like this:

import wikipedia
@service
def search_wikipedia(searchterm=None, return_response=True):
  #summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2})
  summary = task.executor(wikipedia.summary, "Taylor Swift", {"sentences": 2, "auto_suggest": False})
  response_variable = { "summary": summary }
  return response_variable

So I hard coded in the search term but when I call the service I still get:

This error originated from a custom integration.

Logger: custom_components.pyscript.file.wiki.search_wikipedia
Source: custom_components/pyscript/eval.py:493
integration: Pyscript Python scripting (documentation, issues)
First occurred: 5:10:10 PM (1 occurrences)
Last logged: 5:10:10 PM

Exception in <file.wiki.search_wikipedia> line 6: summary = task.executor(wikipedia.summary, "Taylor Swift", {"sentences": 2, "auto_suggest": False}) ^ PageError: Page id "taylor shift" does not match any pages. Try another id!

So it is not honoring the auto_suggest.

To add more information, this is what the wikipedia.summary call looks like:

wikipedia.summary(query, sentences=0, chars=0, auto_suggest=True, redirect=True)

From Wikipedia Documentation — wikipedia 0.9 documentation

This is almost working. Any advice is most appreciated as was your original push forward. Thank you.

Okay I think I made a small mistake in calling the task.executor function. Can you check if this works?

import wikipedia
@service
def search_wikipedia(searchterm=None, return_response=True):
  #summary = task.executor(wikipedia.summary, searchterm, {"sentences": 2})
  funcArgs = {"query": searchterm, "sentences": 2, "auto_suggest": False}
  summary = task.executor(wikipedia.summary, **funcArgs)
  response_variable = { "summary": summary }
  return response_variable

This way all the keys from the dict should be passed as keyword arguments. Hopefully that also fixes the search term still staying as Taylor Swift.

Unfortunately I’m getting this error:

2024-06-08 08:51:14.622 ERROR (MainThread) [custom_components.pyscript.file.wiki.search_wikipedia] Exception in <file.wiki.search_wikipedia> line 6:
      summary = task.executor(wikipedia.summary, **funcArgs)

Welp. I’ve certainly seen more useful error messages than that :joy:
What if you try this?

summary = task.executor(wikipedia.summary, kwargs=funcArgs)

I’ll see if I can play around with some code myself later today, I’ll get back to you if I find something.

Exception in <file.wiki.search_wikipedia> line 6: summary = task.executor(wikipedia.summary, kwargs=funcArgs) ^ TypeError: summary() got an unexpected keyword argument 'kwargs'

Thanks for your persistence. Still not working :frowning: I feel very confident that this will work if somehow we can get the variables passed.

Sorry it took me a bit longer, but I got it working!

@service(supports_response="optional")
def search_wikipedia(searchterm=None, return_response=True):
    """yaml
    name: Search Wikipedia
    description: hello_world service example using pyscript.
    fields:
        searchterm:
            description: What to search for?
            example: Madonna
            required: true
            selector:
                text:
    """
    funcArgs = {"sentences": 2, "auto_suggest": False}
    summary = task.executor(wikipedia.summary, searchterm, **funcArgs)
    response_variable = { "summary": summary }
    return response_variable

I am not fully sure why query cannot be in the **kwargs argument of task.executor honestly. But calling this service from Home Assistant returned a summary of Madonna for me.

1 Like

You are the champion of champions my friend! I cannot thank you enough. This opens so many possibilities for me and the users of my project. We will be able to extend this to other APIs for grabbing data.

If interested, check out the project wiki and/or Discord:

It’s still in beta but really close to release.

1 Like