Pyscript - new integration for easy and powerful Python scripting

Hello. Pyscript and Python newbie.

I want to read a local image, overlay text, and write it back to file (more context)

I was going to use PIL’s for this. I think I need to import it? I don’t think I get how/where imports happen.
I had a browse of the doco’s but I’m confused.

Could anyone point me in the right direction.

My working draft code is below (I’m new to writing functions, but I think I should be able to figure out the rest once I’ve got PIL’s imported?).

Thank you!

Edit: Code slightly refined and imports modified based on craigb’s help below…

@service
def drawtext_onimage(image_in=None, text_to_overlay=None):

  import PIL.Image as Image
  import PIL.ImageDraw as ImageDraw
  import PIL.ImageFont as ImageFont

  #Log message
  log.warning(f"drawtext_onimage: got image_in {image_in} text {text_to_overlay}")

  if image_in is not None and text_to_overlay is not None:

    #load and create image object
    image = Image.open(image_in)
    draw = ImageDraw.Draw(image)

    # specify fontfile and font size
    font = ImageFont.truetype("arial")
    # ImageFont.truetype("/config/www/motioneye_files/GiraffeCam1/all_timelapse/arial.ttf", 20)

    text = text_to_overlay

    # drawing text size
    draw.text((5, 5), text, font = font, align ="left")

    image.save("/config/www/motioneye_files/GiraffeCam1/all_timelapse/lastestGiraffe_wBoxes_cropped_timestamp.jpg")

  elif action == "fire" and id is not None:
      event.fire(id, param1=12, pararm2=80)

Error message in log:

2020-10-24 12:29:07 ERROR (MainThread) [custom_components.pyscript.file.drawtext_onimage.drawtext_onimage] Exception in <file.drawtext_onimage.drawtext_onimage> line 5:
        from PIL import Image, ImageDraw, ImageFont
        ^
AttributeError: module 'PIL' has no attribute 'ImageDraw'

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

Thanks. I did read that but found it confusing when it came to practically implementing it. I’ll review again though.

I’m not familiar with PIL, and I’m just checking it out now. It appears to use some sort of dynamic importing of the submodules, which pyscript doesn’t handle correctly. I’m looking into it.

As a quick workaround, this appears to work:

import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import PIL.ImageFont as ImageFont

Manipulating the image involves reading and writing files (ie, doing I/O), which should not be done in the HASS event loop. It will probably work ok, but you might get some warning messages from HASS, and any blocking from I/O could delay other HASS activities.

So here’s what I’d recommend. Move all the image manipulation code into a true native python module. Your original importing code will work fine in that module. Then the pyscript service can just call the python code in that module, using task.executor to run the code in its own thread. This is described in the documentation.

Here are the steps. Create a new directory config/pyscript_modules. In that directory, create a file image_tools.py that contains this native python code:

from PIL import Image, ImageDraw, ImageFont
 
def draw_text(image_in=None, text_to_overlay=None):
    #load and create image object
    image = Image.open(image_in)
    draw = ImageDraw.Draw(image)

    # specified font
    font = ImageFont.truetype("/config/www/motioneye_files/GiraffeCam1/all_timelapse/arial.ttf", 20)
    text = text_to_overlay

    # drawing text size
    draw.text((5, 5), text, font = font, align ="left")
    image.save("/config/www/motioneye_files/GiraffeCam1/all_timelapse/lastestGiraffe_wBoxes_cropped_timestamp.jpg")

Note: you can’t use pyscript things like log or @service in this file - this is native python code.

Next, your pyscript file in config/pyscripts/image_draw.py should do this:

import sys

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

import image_tools

@service
def drawtext_onimage(image_in=None, text_to_overlay=None):
    log.warning(f"drawtext_onimage: got image_in {image_in} text {text}")
    if image_in is not None and text is not None:
        task.executor(image_tools.draw_text, image_in, text_to_overlay)

This is untested…

1 Like

Much appreciated. I’ll look over this and try to get my head around it. Your earlier post got me a bit further (I edited my code in initial post), but interestingly it errored out with what looked like the same reason I moved away from FFMPEG and on to a Python/pyscript solution - font file errors >>> “your installed PIL was compiled without libfreetype.” (Similar to this: https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/issues/20 )

2020-10-24 15:01:05 WARNING (MainThread) [custom_components.pyscript.file.drawtext_onimage.drawtext_onimage] drawtext_onimage: got image_in /config/www/motioneye_files/GiraffeCam1/all_timelapse/lastestGiraffe_wBoxes_cropped.jpg text test text
2020-10-24 15:01:05 ERROR (MainThread) [custom_components.pyscript.file.drawtext_onimage.drawtext_onimage] Exception in <file.drawtext_onimage.drawtext_onimage> line 19:
        font = ImageFont.truetype("arial")
                                  ^
ImportError: The _imagingft C module is not installed

Anyway I’ll take a closer look and report back with how I get on. Newbie so still learning…

PIL / Pillow doesn’t come with all the libraries it might use. It just makes available those that are already installed. So depending on your platform, you’ll need to install libfreetype then re-install PIL. See, for example:

Ok thanks. I think I get it now. Using hassio so not sure what ability I have to do that but will investigate.

Has anyone got NFC tags working with pyscript. Going to be delving into it (using the mobile app to trigger them currently) but not entirely sure how the mobile app handles them I don’t know if that triggers something pyscript can “watch”.

Wondering if anyone else has figured that part out already.

I don’t have any NFC tags to test this irl, but I’d try to use an event_trigger in pyscript, and listen for the “tag_scanned” event.

Something like this should probably work:

@event_trigger('tag_scanned')
def handle_scanned_tag(tag_id = None):
    log.info(f"Scanned tag with tag_id = {tag_id}")

tag_id should now contain the id of the scanned tag. Other parameters can also be passed in, I assume. I’m using a similar setup to handle responses from actionable notifications on my phone:

@event_trigger("mobile_app_notification_action")
def handle_notification_action(action = None):
    # log.info(f"got mobile notification action event with action={action}")
    if action is not None and action == 'whatever':
        do_stuff()

I’ve since tried that
and

@event_trigger('tag_scanned') 
def handle_scanned_tag(**kwargs):
    log.info(f"got tag_scanned with kwargs={kwargs}")

Neither one works. I’m starting to think that the event is never making it to pyscript. Hopefully someone else can test this cause I really like having all my automations via one thing so I’d love to keep it all in pyscript.

Can you confirm from the logs that a tag_scanned event is actually being fired? You could use the UI Developer Tools -> Event tab to manually fire a test event and check whether or not pyscript triggers on the event.

Do you have an existing yaml automation trigger or template that does work correctly?

Finally, I’m curious which component/integration are you using for NFC tags?

I am not seeing the event with pyscript. I can watch the event in developer tools when I scan the tag, or fire the event manually.

This is from developer tools

Event 0 fired 8:07 AM:
{
    "event_type": "tag_scanned",
    "data": {
        "tag_id": "69086949-ee9c-4512-a55f-b2fdbec2b1f3",
        "device_id": "c4d47bb99a85b2b6"
    },
    "origin": "LOCAL",
    "time_fired": "2020-10-28T12:07:44.272117+00:00",
    "context": {
        "id": "2f8dab22191611ebb7fa3d33f2663215",
        "parent_id": null,
        "user_id": "2e85aebd0a9c436c8567c5435f0d0df8"
    }
}

I haven’t set up any automations for tags yet but I have other pyscript automations that do work.

I’m using the built in NFC tag integration and the android app to read them.

I just went and reinstalled pyscript and still wasn’t getting log messages from my test scripts, went through the documentation again and it had

logger:
  default: info
  logs:
    custom_components.pyscript: info

I had that as homeassistant.component.pyscript.

Changing it to that seems to have fixed it. I’m now seeing the message in the logs when I scan a tag.

Now the next question (I haven’t tried anything yet and about to start work so can’t) is is it possible to get the context info (mainly user_id so I can tell which user scanned the tag) (one use case I’m thinking of is nfc tags on washer/dryer and notify the person that scanned the tag)

To confirm: when you said this

Do you mean that the pyscript trigger function message got tag_scanned with kwargs={...} now appears in the log? If so, that’s great news!

Currently the context isn’t available in pyscript, but @swiftlyfalling has been investigating how to do that, and it’s the next feature we’re working on. See this github issue - feel free to add suggestions and comments there:

yes it is working now. I’ll check out that feature request.

Code was just pushed that adds context as an optional argument to state, event and service functions, although it’s minimally tested. If you use the pyscript master version (eg, install with HACS) you can check it out.

Your function could look like this, with the catch-all kwargs:

@event_trigger('tag_scanned') 
def handle_scanned_tag(**kwargs):
    log.info(f"got tag_scanned with kwargs={kwargs}, user_id={kwargs['context'].user_id}")

or with explicit parameters that come from the event:

@event_trigger('tag_scanned') 
def handle_scanned_tag(tag_id=None, device_id=None, context=None):
    log.info(f"got tag_scanned with tag_id={tag_id}, device_id={device_id}, user_id={context.user_id}")

Looks good so far with the second example

2020-10-28 16:14:11 INFO (MainThread) [custom_components.pyscript.file.tag_test.handle_scanned_tag] got tag_scanned with tag_id=69086949-ee9c-4512-a55f-b2fdbec2b1f3, device_id=c4d47bb99a85b2b6, user_id=2e85aebd0a9c436c8567c5435f0d0df8

Thanks for the quick work now I just have the hard part of coding it to do what I wanted :wink:

So I was able to get a proof of concept working where I can have 1 tag and it toggles which light turns on based on the user that scans it.

Now to create more tags and wrote some more code to get them working.

Any one wish to volunteer to write VSCode integegration for this? :grin:

@craigb any plans, or is it possible to sort our scripts into subdirs?
I am quite OCD with my coding because I am too lazy to “dig” for what I am looking for.

I would love to be able to move all of my pyscripts related to my den into a folder, and even separate by media/server/gaming consoles, etc much like how it already is with my YAML’s.
I eventually want to move AMAP over to this, as I am way better at python logic than I am with YAML logic, and it could become quite cluttered.

Thanks for the great tool. =)

1 Like