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.
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:
Basically: state.persist('pyscript.foo', default_value=None) still results in NameError: name 'pyscript.foo' is not defined when I try to access pyscript.foo.
Any idea what might have changed, or what precondition is required for the state.persist mechanism to work?
@Slalamander Iām back to bother you one more time. Iām wanting to pull data via API and am not sure how to proceed.
import http.client
@service(supports_response="optional")
def get_dadjoke(return_response=True):
"""yaml
name: Get Dad joke
description: Gets a random dad joke from api
"""
conn = http.client.HTTPSConnection("dad-jokes-by-api-ninjas.p.rapidapi.com")
headers = {
'x-rapidapi-key': "get your api key at rapidapi",
'x-rapidapi-host': "dad-jokes-by-api-ninjas.p.rapidapi.com"
}
conn.request("GET", "/v1/dadjokes", headers=headers)
res = conn.getresponse()
response_variable = res.read()
return response_variable
Iām basing this on a sample script from rapidapi which works in regular python (I have a valid api key) and trying to mix in what you gave me for the wikipedia part.
Iām a bit confused on the task.executor portion. I know just a bit about python so excuse me. Do I need to use the task.executor for all lines that use that āconnā variable or certain ones, or none? Seems like a pretty simple script but not so much for me. I think once I get one of these working Iāll be able to follow suit for others and this will open up a lot of things. Thanks for your time and help!
The task.executor is there for functions that block the event loop. Iām not fully sure how the methods for conn function, but I think only conn.request should be run in the executor, perhaps though also getresponse.
However, I think it may be easier to use the requests module. I believe that is included by default in Home Assistant installations. The code would become something like the following I think.
import requests
@service(supports_response="optional")
def get_dadjoke(return_response=True):
"""yaml
name: Get Dad joke
description: Gets a random dad joke from api
"""
headers = {
'x-rapidapi-key': "get your api key at rapidapi",
'x-rapidapi-host': "dad-jokes-by-api-ninjas.p.rapidapi.com"
}
r = task.executor(requests.get, url, headers=headers)
response_variable = r.json()
return response_variable
If you need to pass an authentication argument, you can add auth=auth.
websocket_api script: Error executing script. Error for call_service at pos 1: Failed to process the returned service response data, expected a dictionary, but got <class 'NoneType'>
websocket_api script: Error executing script. Error for call_service at pos 1: Failed to process the returned service response data, expected a dictionary, but got <class 'list'>
I wish I was better at troubleshooting. Is this failing to connect and therefore is returning the dictionary error because the response is empty?
EDIT By the way, I tried changing from https to http and got this:
message: Please use HTTPS protocol
as my response so it appears to at least be contacting the server. Wondering if it is getting those headers or not. Again, Iām a novice here. Thanks again for the help.
EDIT2 This is an example of what the python script (not pyscript) outputs for reference:
[{"joke": "What do you put on a lonely grilled cheese sandwich? Provolone, but only if you have it\u2019s parmesan."}]
Ah thatās an easy fix actually, itās just a dict put into a list, and Home Assistant only accepts dicts as response variables. I didnāt notice the json() function from requests always returns a list with json data (See the docs here). Assuming resp is the output of the python script, you should be able to return it to Home Assistant as such:
resp = r.json()
return resp[0]
This assumes youāre only getting one joke at a time. If itās possible to get more jokes, you could also do the following:
This way, the data under "jokes" will always be a list, so youād have to extract each joke individually in Home Assistant, even if it is just the one. I donāt know of the top of my head how to do that in Jinja2, so let me know if you run into trouble with that.
EDIT:
Come to think of it, your first error shows a Nonetype returning. Iām not fully sure what the case is with that. It may be a good idea to check for statuscodes, in case you get errors. See here.
if r.status_code == requests.codes.ok:
##Code here with how you want to return the response
else:
resp = {"error": r.status_code, "data": r.json()}
return resp
(You donāt need to return errors, but it may be useful while debugging).
Once again, thank you for this. I will be able to use this method to pull lots of different data from api. You were right, the fix was very easy and I guess the error I provided may have included something from a previous run.
So I thought Iād give myself a little more practice and modify the wiki search to use a different method:
import requests
@service(supports_response="optional")
def search_wikipedia_new(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:
"""
url = "https://en.wikipedia.org/api/rest_v1/page/summary/Madonna?redirect=true"
r = task.executor(requests.get, url)
if r.status_code == requests.codes.ok:
response_variable = r.json()
return response_variable[0]
else:
response_variable = {"error": r.status_code, "data": r.json()}
return response_variable
I feel like Iām not changing much short of the url and not using the headers but I am once again getting the error:
Failed to process the returned service response data, expected a dictionary, but got <class 'NoneType'>.
Is there an easy way to view what is being received? Itās super hard to troubleshoot this without being able to somehow get data out short of the return variable route.
As a test I wrote this in regular python and it works as expected:
Debugging pyscripts is a real pain yeah I tend to use print statements myself and look them up in the log. However they accumulate and different log messages tend to be filed under the same header in the Home Assistant logs
You could try setting up the Jupyter Notebook environment, as that allows real time code execution I believe, so itād be easier to check the print statements in the notebook. However I havenāt used it much, so I canāt really help much with setting it up. I believe the pyscript docs explain it quite a bit tho!
Those print statements sounds like my kind of troubleshooting as Iām definitely familiar with that approach. I didnāt realize that was an option. Iām guessing I need to set logging to a certain level to see that?
I tried the Jupyter Notebook approach but itās not easy with my environment. Iām running on Docker and it is a struggle for me to figure out how to get that in place. I did set up a VM and was able to install JN but now having issues getting that going. Struggles.
You can also use log.warning("something happened") which should probably log it in HA without needing additional configuration (I believe warning is the default level for integrations). For the print statement, the loglevel should be debug.
import requests
@service(supports_response="optional")
def search_wikipedia_new(searchterm=None, return_response=True):
"""yaml
name: Search Wikipedia New
description: hello_world service example using pyscript.
fields:
searchterm:
description: What to search for?
example: Madonna
required: true
selector:
text:
"""
url = "https://en.wikipedia.org/api/rest_v1/page/summary/" + searchterm.replace(" ","_") + "?redirect=true"
r = task.executor(requests.get, url)
if r.status_code == requests.codes.ok:
wiki_data = r.json()
title = wiki_data['title']
thumbnail = wiki_data['thumbnail']['source']
extract= wiki_data['extract']
response_variable = {"title": title,"thumbnail": thumbnail, "extract": extract}
return response_variable
else:
response_variable = {"error": r.status_code, "data": r.json()}
return response_variable
This is the push that I needed! Managed to figure it out once I could āseeā what was going on. Again, thank you @Slalamander ! Hopefully this is all that Iāll need to really hit the ground running.
I use log.info('Here is some string to help with debugging') in the .py file and then use the Log Viewer add-on to view them. Itās instant and works well for me.
Out of curiosity, where do you see the print statements in HA?
P.S. PyScript has been a game changer for me. I struggle coding in YAML. So THANK YOU to the developers and contributes to this project!
I run Home Assistant container, so I canāt use any add-ons.
The print statements show up in the full logs, which you can access via your-url/config/logs and then scroll down to where it says āload full logsā. Since itās all the logs itās not as clear though as the base log view.
Also it requires reloading every time you print something new, so I think the add-on is a much better option for anyone that has it available
@Slalamander and everyone. I am wanting to use pyscript to create temporary timers instead of creating a bunch of helpers. I am writing to see if this is even a good idea.
Hereās what I would like to do:
create a temporary timer with a given duration in seconds and also have a name
the timer would run down or a time based on the duration would be set
on expiring or that time arriving pyscript would create an event that would state the timer has expired and provide the name given when created
That would give basic functionality. Extended functionality would include these:
ability to cancel the timer
ability to delete the timer before expiring
ability to report the remaining time on query
Of these, the most important would be time remaining. I understand that these extended functionality could be difficult to accomplish. Basic functionality would at least get things going though.
I am not sure how well itād work, but my best bet would be using asyncio.sleep to set the timers. Iām not sure how you would be able to report on the time remaining though, without setting up a sensor or something.
Anyhow, what Iād attempt would be making a service that creates a new asyncio task. Determine the time it should finish at, and put that in a global variable or a home assistant sensor keeping track of set timers (Like, a dict with task names and their end times). Tasks can be cancelled (And thus deleted) relatively simply, though in that case there is not much difference between cancelling and deleting. The tasks can all have a callback that publishes the event.
I havenāt combined asyncio with pyscript, however it should be able to play nicely with it since Home Assistant is also asynchronous (Itās also why pyscript requires some functions to called using the executor statement).
Which is basically what youāre trying to do now, but with names, if Iām correct right? If so, you can put the event.fire either in a seperate function, or add a field to the function right now that allows you to set the name, and incorporate that in the event.
Idk if I already linked this website, but I really like it. Itās clear and has a ton of examples: Start Here - Super Fast Python
I think going through step 2 of the asyncio stuff should be enough to build the timers (provided my idea actually works)
Thanks for your continued help. Iāve been so tied up with the project and doing a million different things I forgot I had even brought this up and that you had replied.
I read through the two sections you mentioned and have a better understanding. Really well written. Thanks for the link.
So this brings me back to your script. I can call the starttimer with duration and I believe it is running as expected. The issue comes in when it is time to fire the event. I am getting this error:
Exception in <file.saltimer.sal_timer> line 7: event.fire('viewassist_timer_expired', action=action, id=id, duration=duration) ^ TypeError: a coroutine was expected, got None
From what I am reading from the link you mentioned, doesnāt this been to be awaited as well?