Automated time lapse videos with Home Assistant

Ever wanted to set up a time lapse video? 

I’m going to show you how to do it with home assistant using a 10 dollar or less camera!




In this tutorial we will cover: 
  • Flashing the camera 
  • Setting up the camera in Home Assistant 
  • Setting up the automation 
  • Setting up a script to combine the images into a video 
  • Setting up a service to call the script 

Flashing the camera




For my project I am using:
  • an AI-Thinker clone that I got from Amazon (3 for $15.88)
  • PlatformIO with VS Code
  • an example from the Arduino board package downloaded from github
  • Home Assistant (hass)
  • an FTDI adapter for uploading the code
There are a number of methods that can be used to pull the camera into Home Assistant.

First, you can use the ESPHome integration with a few lines of yaml code, and while this method tightly integrates with hass, I didn't find a way to access the camera outside of the hass environment. Check the docs for this camera at https://www.esphome.io/components/esp32_camera.html?highlight=camera to see how simple it can be.

Or, you can use the code from the Arduino examples from the ESP32 board package from Espressif. If you don't have the board installed in the Arduino IDE, you can download it from the github repository 

This is the method I chose to use(the Arduino example), as it allowed me to access the camera via a simple web interface that makes changing a number of settings very simple.

The coding

Once you have PlatformIO installed with VS Code, you can import the project from the ESP32 board package by choosing "Import Arduino Project" from the PlatformIO home screen inside VS Code



Choose the board to use (AI Thinker ESP32-CAM), then navigate to the folder where you extracted the repository, and drill down into the libraries/ESP32/examples/Camera/CameraWebServer folder; click Import


VS Code started complaining that it didn't handle .ino files properly, so I renamed the main file CameraWebServer.ino to CameraWebServer.ino.cpp to make it happy. Once the project is imported and set up, I only had to change a few lines of code. First make sure the correct board is defined:

1
2
3
4
5
6
// Select camera model
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_ESP_EYE
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER

Then the information to connect to wifi:

1
2
const char* ssid = "HomeAssistantIsAwesome";
const char* password = "NeverUsePasswordAsAPassword";

And finally, since I didn't want the image size that was set in the code:

1
2
3
  //drop down frame size for higher initial frame rate
  // s->set_framesize(s, FRAMESIZE_QVGA);
  s->set_framesize(s, FRAMESIZE_VGA);

Update: in the version of code I just downloaded I needed to remove the following lines of code from the CameraWebServer.ino from the beginning. If you don't see these lines, you won't need to remove them.

1
2
//Disable Example for now
#if 0

and then from the end:
   
1
2
3
4
#else
void setup(){}
void loop(){}
#endif

Also, in the app_httpd.cpp after the comments at the beginning
     
1
#if 0

and at the end:
     
1
#endif

Wiring the board

Here's the wiring diagram for programming the board. I am using both USB power from an USB breakout board, as well as the power from the FTDI adaptor because in my attempts to use just the FTDI adaptor, the board was having brownouts after flashing. Attempting to monitor the board after restarting was impossible and it wasn't working anyway due to the brownouts.


Flashing the board

Now that the code is complete, and the board is all wired up ready to flash, let's upload the firmware.


Click on upload and monitor, and the code should compile and start to upload. Once it has completed, the board will reset. At this point remove the wire between IO0 and GND, hit the reset button, and you should see the board start connecting to WIFI, and showing you the connected IP address.


Connect to the address shown in the monitor output (or find the device attached to your wireless router using the interface for your router), and you should see the webpage for the camera (I've already clicked on "Get Still" in this image).




If you've made it this far, we're ready to set up Home Assistant!

Setting up Home Assistant

To get the camera into Home Assistant for this tutorial, we need the ability to edit the config files. For my setup, I'm using the Visual Studio Code plugin. Since adding this camera is going to require restarting home assistant to take effect, we're going to do two steps in one so we don't have to restart twice.

Add the camera and the shell command

In the configuration.yaml add the following, change the name to whatever you want your camera to be called, for the tutorial I'm just using esp32cam. Also make sure to change the IP to be whatever your camera's IP is that got assigned.

1
2
3
4
5
6
7
camera:
  - platform: mjpeg
    name: esp32cam
    mjpeg_url: http://10.0.0.144/capture?

shell_command:
  do_movie: nohup /bin/bash /config/doffmpeg.sh "{{ start_date }}" "{{ end_date }}" "{{ cam_path }}" &

Go ahead and restart Home Assistant now so that the camera will show up for the next part. DON'T FORGET to check the configuration first.




Set up the automation for capturing the images

Next we'll set up the automation for the snapshots. Navigate to Configuration -> Automations in Home Assistant and click on "+ ADD AUTOMATION" in the bottom right corner. Then choose "Start with an empty automation" on the dialog that comes up.


Give your automation a name, then choose the "Time Pattern" trigger type, and put an asterisk in the Minutes field. This will take a capture every minute on the top of the minute.


Scroll down and set up any conditions you want. In my example I have a grow light that is only on from 07:00 until 20:00 so I don't need any shots outside that timeframe.


Next comes the Actions section, and this is important to get correct, or the next steps will not work without modifications. Click on the 3 dots on the right hand side, choose "Edit in YAML" and paste in the following YAML code

1
2
3
4
5
6
7
service: camera.snapshot
target:
  entity_id: camera.esp32cam
data:
  filename: >-
    /media/esp32cam/{{ now().strftime("%Y%m%d") }}/esp32cam_{{    
now().strftime("%Y%m%d-%H%M%S") }}.jpg

Make sure to change the entity_id to match the name you assigned your camera in the configuration. yaml, and also change the name in the filename. This becomes more important as you add cameras to keep the files from overwriting each other or becoming intermingled. Save it and we're done with the automation.

By default the automation will be turned on. I'll let it run so I have some images to piece together later.

We can tell it's working by navigating to Media Browser -> Local Media -> <camera name>

Setting up the bash script to build the video


In the config directory on Home Assistant, create a new file named 'doffmpeg.sh' and paste in the contents below. Save the file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/bash
###################################################
# This script takes three arguments and will ignore the "&" parameter that is passed
# because nohup is used to avoid the 60 second limitation placed on
# shell scripts run in home assistant, and can be called in 3 different scenarios 
# shown below. The date format should be YYYYMMDD
#
# doffmpeg <start date> <end date> <cam name> - this will generate all the videos from 
# <start date> to <end date> and all dates in between
#
# doffmpeg <start date> <number 1 or above> <cam name>- this will generate all the videos
# from <start date> to <start date + number> i.e. if the date is 20210301 and the 
# second argument is 3, it would process 20210301, 20210302, 20210303, 20210304  
#
# NOTE: the script will not process across months, so the two above would not work
# if the <end date> was another month than <start date>, or if the second argument 
# is more than the difference between <start date> and the last day of the month.
# For instance, a start date of 20210329 and the second argument of 3
#
#doffmpeg <start date> 0 <cam name> - this will generate only the date passed as the start
# date, and would be likely used by an automation to generate the video every day
# at the end of the day. example: 20210329 0 "esp32cam"

callFFMpeg(){
    ffmpeg -pattern_type glob -i "/media/$2/$1/*.jpg" \
    -c:v libx264 -vf fps=25,format=yuv420p \
    "/media/$2/$1_out.mp4"
}

if [[ ${#3} > 0 ]] && [[ "${3}&" != "&" ]]
then
    if [[ ${#1} == 8 ]] && [[ ${#2} == 8 ]]
    then
        for (( MY_DATE=$1; MY_DATE<=$2; MY_DATE++ ))
        do
            callFFMpeg ${MY_DATE} ${3}
        done
    else
        if [[ ${#2} == 1 ]] && [[ ${2} > 0 ]]
        then
            for (( MY_DATE=$1; MY_DATE<=$1+$2; MY_DATE++ ))
            do 
                callFFMpeg ${MY_DATE} ${3}
            done
        else
            MY_DATE=$1
            callFFMpeg ${MY_DATE} ${3}
        fi
    fi
fi

As noted in the comments, this script takes 3 arguments, for various configurations, but we're going to use the last one passing the current date, but using a template so it gets the current date. We'll set it up to run at 1 minute before midnight so it gets the entire day worth of images, but also the current date for the date stamp. 

We can test the script by calling the service we created earlier. Navigate to Developer Tools -> Services and choose "Go to YAML mode"


Paste in the following code (change the date to match your files):

1
2
3
4
5
service: shell_command.do_movie
data:
  start_date: 20210426
  end_date: 0
  cam_path: "esp32cam"

Click on CALL SERVICE, wait a minute to let the script run, then navigate to the Media Browser -> Local Files. Go into the folder named after your camera, and check to see if the new video populates. If not, check for errors in the logs and see if you have some errors in the code somewhere.

If all is well, you should have a nice time lapse video from the captured images. Now let's set up another automation to call this script every day just before midnight.

Setting up the automation to build the videos daily


Now that we have all the pieces, let's tie them all together to fully automate the time lapse production. We'll start a new automation, again we'll use a Time Pattern trigger type.


We'll set the hour as 23, and the minute as 59, this way the automation will fire every day at 23:59, just before the date changes. For testing, you might put a time just a few minutes from now to see if it runs properly, and then set it to 23:59 once you know it works.

Set a condition if you like, but we don't need one.

In the Actions section, again choose to use the YAML editor, and paste in the following code, adjusting the cam_path to fit your application.

1
2
3
4
5
service: shell_command.do_movie
data:
  start_date: '{{ now().strftime("%Y%m%d") }}'
  end_date: 0
  cam_path: esp32cam

Save the automation, and we're done. We now have an automation that captures images at an interval we set, and then compiles them all together into a video at the end of the day!

I hope you found this tutorial helpful, I learned a lot putting it together.



NOTE: if you get an error message about ffmpeg missing or not being found, you may need to add the following to the configuration.yaml (I don't think I needed to add it because I am using the Frigate addon in Home Assistant, which I love and highly recommend):

ffmpeg:

NOTE 2: As posted in the comments, there are likely to be issues between versions of Home Assistant based on which operating system you are using under the hood. As I am running mine on HassOs (both standalone on a laptop, as well as a Hyper-V VM for my testing environment), my paths are different than those required to run in Docker on Ubuntu, for instance:

path to store images
path for scripts
execution of command encapsulated in single quotes (not the "nohup" and not the "&")

Thanks to Todd for all his work in troubleshooting and updating me in the comments.

NOTE 3: "For anyone with the issue of ffmpeg not being found despite adding it to the configuration, what fixed it for me was downloading the static build of ffmpeg and copying it to /usr/bin"

Thanks to Aeleos for the tip over on YouTube.



Comments

  1. Hello,
    Thanks for sharing this. It's exactly what I need for a new house build project we are having done. I'm using an Amcrest IP cam but the only still option I have is for a .cgi image rather than an .mpg file. Are there any ways to work around this that you can think of?
    Regards
    Mark

    ReplyDelete
    Replies
    1. I've never seen an image format for .cgi, usually .cgi is a script that runs on a web server in my experience. If you call the service to capture an image it saves in this format?

      Delete
    2. I'm using an Amcrest IP cam as well. Finally got the automation to create the files at the specified interval working. My problem is the do_movie script. I'm running HA Core on Docker running on Ubuntu. I have already changed the shell to /bin instead of /usr/bin for bash in the script. I've also had to modify how the images get saved (instead of /tmp, I had to preface my path with www, then it worked, and put it in the www folder of my docker HA instance). I've also modified the path in the do_movie script to be the same. Here is the error when I try to run the do_movie shell script...

      Service shell_command.do_movie does not match format . for dictionary value @ data['sequence'][0]['service']. Got 'shell_command.do_movie '

      Delete
    3. And to continue that troubleshooting... I am able to exec into the homeassistant instance, and run the script from the bash prompt there, and it works. It just does not work as a called shell_script from inside the HA application, with the above error showing up in the HA logs.

      Delete
    4. Are you able to call the service from the developer tools, or is that what you are talking about here?

      Delete
    5. That's what I'm talking about, here... developer tools gets that error. I have not yet set up the automation for the do_movie.

      Delete
    6. I think I might have figured it out. I changed your do_movie line in configuration.yaml from:
      do_movie: nohup /bin/bash /config/doffmpeg.sh "{{ start_date }}" "{{ end_date }}" "{{ cam_path }}" &

      to:
      do_movie: nohup /bin/bash -c '/config/doffmpeg.sh "{{ start_date }}" "{{ end_date }}" "{{ cam_path }}"' &

      That worked from developer tools. The first run of a scheduled automation is tonight.

      Delete
    7. Great, as soon as you find out, let me know, and if it works I'll update the post to reflect that as a potential requirement for various operating systems other than HassOs.

      Delete
    8. Yep... it all worked.
      I didn't recall seeing (or hearing in your youtube video) that your config was HassOS. But, I figured there was gonna be some differences with my HA Core in Docker on Ubuntu. Those are:
      path to store images
      path for scripts
      execution of command encapsulated in single quotes (not the "nohup" and not the "&")

      I also plan to work on your creation script a bit... gonna have it delete the folder with all the images after it creates the movie, and a few other changes for different time-lapse down the road.
      Thanks for the posts, and the scripts!

      Delete
  2. I've followed all the steps and am having some trouble with my system crashing while creating the .MP4. I can see that a video is created, but it's corrupted and won't play. I'm running HA OS on an RPi 4 and suspect that the RPi is just not powerful enough to process it?

    ReplyDelete
    Replies
    1. First thing I would try would be using a small set of files to see it that works, then if it does, increase the size until you find the fail point.

      Delete
  3. thanks for the tutorial - is it possible to combine the dates into one single video instead of one video for every day?

    ReplyDelete
  4. I have problem uploading:
    src/app_httpd.cpp: In function 'void setupLedFlash(int)':
    src/app_httpd.cpp:1393:5: error: 'ledcAttach' was not declared in this scope
    ledcAttach(pin, 5000, 8);
    ^~~~~~~~~~
    src/app_httpd.cpp:1393:5: note: suggested alternative: 'ledcAttachPin'
    ledcAttach(pin, 5000, 8);
    ^~~~~~~~~~
    ledcAttachPin
    *** [.pio\build\esp32cam\src\app_httpd.cpp.o] Error 1

    ReplyDelete

Post a Comment