A completely ridiculous way to create a gif from security camera images

I decided it would be cool to display a time-lapse video using my Blink security camera and ended up over-complicating it to all hell because… why not? There are many, WAY easier, ways to do this. But developing a system to pass arbitrary condiments is way more fun :laughing:

My setup

I’m running home-assistant in a docker container on UnRAID and have a fairly beefy server (I have like 20+ containers running various things, plus two or three VMs). My full home-assistant config can be found here for those interested, but I’ll recreate the important bits in this post.

Automatically take pictures

The first step here was to create an automation to automatically take a picture using my blink camera. This was pretty straight-forward, but required some fiddling to get the right interval (without destroying the battery). I settled on a picture every twenty minutes.

I also set a condition here. Since my blink camera does not have an IR LED for night vision (I’m using an indoor camera outdoors with the illuminator turned off) I really can’t get any usable pictures at night. So I stop taking pictures between ~midnight and either 4am or 30 minutes prior to sunrise, whichever is later (almost always sunrise, but whatever).

I then call a script to take a picture with the camera (we’ll get to that in a minute) and then use the camera.snapshot service to save the camera to a file. My /images directory is just a mounted volume to somewhere else on my server (again, we’ll get to that later). The filename format just ensures that I have a unique, and useful, filename for each image I take.

alias: Blink Automatically Take Picture
trigger:
  - platform: time
    minutes: '/20'
    seconds: 00
condition:
  condition: and
  conditions:
    - condition: time
      before: '00:10:00'
      after: '04:10:00'
    - condition: sun
      after: sunrise
      after_offset: '-00:30:00'
action:
  - service: script.blink_trigger_camera
  - delay: 00:00:10
  - service: camera.snapshot
    data:
      entity_id: camera.blink_living_room
      filename: /images/raw_images/blink_living_room_{{now().year}}{{now().month}}{{now().day}}_{{now().hour}}{{now().minute}}{{now().second}}.jpg

The script is even more simple than the automation. I just call the blink.trigger_camera service to tell my camera (which is named “Living Room” despite living outside… I was too lazy to change the name) followed by a brief delay to ensure the command completed, and then I force an update of the blink information within home assistant.

blink_trigger_camera:
  alias: Script - Blink Trigger Camera 
  sequence:
    - service: blink.trigger_camera
      data:
          name: "Living Room"
    - delay: 00:00:05
    - service: blink.blink_update

Ok, now what?

Neat. So now I have pictures every twenty minutes…how do I turn it into a gif? What I do here is over-engineer the crap out of it. I figured I’d need to use a tool like ffmpeg or imagemagick to create a gif which meant I’d need to run them in a separate docker container. Continuing this thought, I figured i may be useful in the future to have a generic container that could run any python script I’d ever want, which I could then easily configure with cron (without physically editing the crontab). So, naturally, I made my own generic python container with configurable scheduling… like I said, I over-complicated it, but it works great :+1:

Enter pycron

The docker container I made is dubbed pycron and the source repository is here: fronzbot/docker-pycron. It’s (in my opinion) very easy to use. You simply map a /work volume where you add a config.yaml folder that hold all the scripts you want scheduled AND when you want them to be scheduled. Any edits to your scripts do not require a restart to take effect, although editing the schedule does require a container restart. In addition to this, installing custom pip modules can be done at container start by adding them to the my_requirements.txt file in your /work volume. There is also a /share volume that I added specifically to share among other docker containers. So for home-assistant, I created a directory called /share/hass that I then mapped to home-assistant as /images (which is the directory from the automation earlier).

My configuration for this can be found here. I’ll refrain from posting the python scripts since it’ll just take up too much space on the post, but feel free to check them out.

First up is my configuration file. Here I have two scripts: one to create my gif, the other to back it up. I want to create a new gif every hour (more frequently than that wasn’t very useful) and I wanted to save one every day so I could eventually string them together for a yearly timelapse (or something). Thus, my pycron config.yaml file looks like this:

make_gif:
  schedule:
    hours: 1

save_gif:
  schedule:
     days: 1

As for the scripts, in the make_gif script I designate a maximum number of images to retain (anything over is backup up to a different directory) and I give it a target length of 10 seconds. I use the gifsicle package to optimize the gif which helps reduce the file size by about 5x from the original.

It’s worth adding how I actually create the gif. The first thing I do i retrieve all the files in my save directory and sort them based on the timestamp (so they are absolutely in chronological order). Then I use imageio to create the gif and use a gifsicle command to optimize. The following is a simplified version of what can be found in my script.

import os
import imageio

IMAGE_DIR = '/share/hass/raw_images'
BACKUP_DIR = '/share/hass/backup'
SAVE_DIR = '/share/hass/gifs'

# Sort the files
files = os.listdir(IMAGE_DIR)
fullfiles = [os.path.join(IMAGE_DIR, name) for name in files]
sortedfiles = sorted(fullfiles, key=os.path.getmtime)

output = os.path.join(SAVE_DIR, 'temp.gif')
images = []

# Calculate framerate for 10 second gif
framerate = int(len(sortedfiles) / 10)

# Create the gif
with imageio.get_writer(output, mode='I', fps=framerate) as writer:
    for filename in sortedfiles:
        image = imageio.imread(filename)
        writer.append_data(image)

# And now optimize the gif
final = os.path.join(SAVE_DIR, 'output.gif')
command = "gifsicle -O3 --colors 128 --resize-width 512 {} > {}".format(output, final)
os.system(command)
os.remove(output)

Displaying the gif in Home-Assistant

So I have my gif saved, but now I need to display it in home-assistant. For this, I use the local file camera platform. I’ve already mapped the directory where I save my gif to home-assistant, so I have the following in my home-assistant configuration.yaml file:

camera:
  - platform: local_file
    name: blink_gif
    file_path: /images/gifs/output.gif

And here is an example gif that is created (this one is actually at 30mins/picture rather than 20, but you get the idea).

blink_2018_11_05

But… why? :confused:

Valid question. Originally I didn’t really want to make a generic container to run and schedule my python script, but I started seeing potential benefits to it long term (mostly unrelated to home-assistant). On top of that, I’m very much a docker n00b so I figured it would be a good learning experience in creating and managing a container. I’m pretty happy with how it turned out, despite the seemingly roundabout way I got there… I’m like one step away from this being a Rube Goldberg machine.

Anyways, hopefully someone else enjoys this or finds it useful. Even if your conclusion is “this is the stupidest way to accomplish that task”, at least you now know one way not to do it! :smile:

11 Likes

I like it.

I can use this to string together a few snapshots from my cameras I use to trigger an alarm system. Right now it just sends one still.

Hey @fronzbot, I stumbled upon your topic here because I wanted to be able to have my Blink cameras automatically update the picture so I don’t have to keep going into the app and doing it myself. I didn’t want to do the gif part like you did, just update the picture on the two cameras I have. I created a script like you did:

blink_trigger_cameras:
  alias: Script - Blink Trigger Cameras 
  sequence:
    - service: blink.trigger_camera
      data:
          name: "Front Camera"
    - delay: 00:00:10
    - service: blink.trigger_camera
      data:
          name: "Back Camera"
    - delay: 00:00:10
    - service: blink.blink_update

And then I created an automation:

- alias: Blink Automatically Take Picture with Cameras
  trigger:
     platform: time
     minutes: '/20'
     seconds: 00
  action:
    - service: script.blink_trigger_cameras
    - delay: 00:00:10
    - service: camera.snapshot
      entity_id:
          - camera.blink_front_blink_camera
          - camera.blink_back_blink_camera

However I’m getting a couple of weird errors in my log file. The first is this:

Error doing job: Task exception was never retrieved
Traceback (most recent call last):
  File "/usr/src/app/homeassistant/helpers/script.py", line 130, in async_run
    await self._handle_action(action, variables, context)
  File "/usr/src/app/homeassistant/helpers/script.py", line 172, in _handle_action
    action, variables, context)
  File "/usr/src/app/homeassistant/helpers/script.py", line 261, in _async_call_service
    context=context
  File "/usr/src/app/homeassistant/helpers/service.py", line 81, in async_call_from_config
    domain, service_name, service_data, blocking=blocking, context=context)
  File "/usr/src/app/homeassistant/core.py", line 1101, in async_call
    processed_data = handler.schema(service_data)
  File "/usr/local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 267, in __call__
    return self._compiled([], data)
  File "/usr/local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 589, in validate_dict
    return base_validate(path, iteritems(data), out)
  File "/usr/local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 427, in validate_mapping
    raise er.MultipleInvalid(errors)
voluptuous.error.MultipleInvalid: required key not provided @ data['filename']

And then every now and again I get a “Cannot obtain new token for server auth” error. Any ideas what would be causing these two errors?

So the first error is because you’re calling the camera.snapshot service incorrectly docs. The “filename” attribute is a required part of that service call (and that’s exactly what the error says: the ‘filename’ key was not provided as data for the service). Add that in and you should be set :+1:

As for the token auth error, that occurs if your Blink token has expired and an error occurs upon a re-authorization attempt (could be an internet issue or an issue with Blink’s servers). I assume you are on 0.84+? If not, some bugs were fixed where that error was logged too many times so if you’re on pre-0.84 that could be part of the problem. The error itself just means that an attempt was made to update info from Blink, but the request failed. Usually it will resolve upon the next scheduled blink update.

Ah, gotcha. I don’t really care about saving a picture like you were doing, just wanted my camera to update their pictures every 20 minutes. I knew the error was referring to a filename, but I was confused because I wasn’t including that option at all. Would have been a little more intuitive if the error referred back to the service, but I guess that’s partially my fault for just taking your code and not trying to figure it out first.

I went ahead and removed the camera.snapshot service from the automation and it seems to be working, or at least that error went away. This last time I still got the token error. I am running 0.84.6 currently. I did see the bug on your git about the error, although the way I read it on there made it seem like it was just a cosmetic error?

Not to get too far down the rabbit hole here with that error, but is it possible that if two different automations are running at the same time connecting to the Blink servers that could cause the token error as well? I have an automation that disables motion detection at 6am on my front camera, and if that runs at the same time as the picture automation, I think that might cause some conflict as a couple of times the motion detection is not disabled on my front camera, or at least since I’ve set this up.

Yes, that issue was a cosmetic one- the error spammed a lot in the logs, even if auth was successful.

As for the concurrent automations… it’s certainly possible that would cause it. Are you seeing the error happen at 6am? If so, a temporary fix would obviously be to shift the automation time to not collide with the picture automation. The long-term fix would be the need to include request queuing in the main library (which should happen even if this isn’t your problem).

It wasn’t always happening at 6am, although it did happen a couple of times which made me think. I haven’t seen in it the last couple of days, so it might just be a coincidence.

Thanks again for your help!

This cracked me up :slight_smile: nice one…