Software engineer with a focus on game development and scalable backend development
I had wanted to get a couple of cameras to watch over my house and make sure everything was as I expected. A couple of requirements I had when looking were being able to use them without any cloud connection, and being compatible with self-hosted solutions, which usually means having RTSP support. Looking at some commercial products, it seemed hit or miss as to whether they would support any custom firmware which would mean needing advanced networking setup to cut them out from the internet but also allowing local networking, for these reasons I decided for now to give making my own a go…
When looking online for cheap camera hardware that can run custom code, there are several options available; however, I wanted to try out something cheap to begin with, as I don’t really need professional-level CCTV, just something to be able to check the house over with. The most popular options online seem to be a RPi-based camera running full Linux or an ESP32-based camera running some custom code. As I didn’t want this to be overly complex and I already run a couple devices for other smart home applications, I ended up going with the ESP32.
To start with, I found a few open source firmwares that supported RTSP and could easily be installed on the ESP32; however, the more I tinkered with them, the more I wanted a unified platform to just manage all of my ESP devices, so I decided to go with EspHome, with the added benefit that it makes it much easier to apply newer versions of the EspHome SDK than other firmwares I had found.
A lot of the config is fairly standard across other ESP devices; apart from the camera section, the EspHome SDK provides the camera output in MJpeg format, which generally means lower quality and framerate. I don’t really mind this, as I mainly want a minimum of 4 fps just to be able to get an idea of what is happening. Likewise, I am happy to give up some resolution in exchange for image quality; these are all things that can be configured, and thanks to a few examples by other people, I also managed to add controls for the lights and LEDs on the board as well. The full configuration for EspHome is here:
esphome:
name: kitchen-espcam
friendly_name: kitchen-espcam
esp32:
board: esp32dev
framework:
type: arduino
web_server:
port: 80
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret kitchen_espcam_api_password
services: # change camera parameters on-the-fly
- service: camera_set_param
variables:
name: string
value: int
then:
- lambda: |-
bool state_return = false;
if (("contrast" == name) && (value >= -2) && (value <= 2)) { id(espcam).set_contrast(value); state_return = true; }
if (("brightness" == name) && (value >= -2) && (value <= 2)) { id(espcam).set_brightness(value); state_return = true; }
if (("saturation" == name) && (value >= -2) && (value <= 2)) { id(espcam).set_saturation(value); state_return = true; }
if (("special_effect" == name) && (value >= 0U) && (value <= 6U)) { id(espcam).set_special_effect((esphome::esp32_camera::ESP32SpecialEffect)value); state_return = true; }
if (("aec_mode" == name) && (value >= 0U) && (value <= 1U)) { id(espcam).set_aec_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
if (("aec2" == name) && (value >= 0U) && (value <= 1U)) { id(espcam).set_aec2(value); state_return = true; }
if (("ae_level" == name) && (value >= -2) && (value <= 2)) { id(espcam).set_ae_level(value); state_return = true; }
if (("aec_value" == name) && (value >= 0U) && (value <= 1200U)) { id(espcam).set_aec_value(value); state_return = true; }
if (("agc_mode" == name) && (value >= 0U) && (value <= 1U)) { id(espcam).set_agc_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
if (("agc_value" == name) && (value >= 0U) && (value <= 30U)) { id(espcam).set_agc_value(value); state_return = true; }
if (("agc_gain_ceiling" == name) && (value >= 0U) && (value <= 6U)) { id(espcam).set_agc_gain_ceiling((esphome::esp32_camera::ESP32AgcGainCeiling)value); state_return = true; }
if (("wb_mode" == name) && (value >= 0U) && (value <= 4U)) { id(espcam).set_wb_mode((esphome::esp32_camera::ESP32WhiteBalanceMode)value); state_return = true; }
if (("test_pattern" == name) && (value >= 0U) && (value <= 1U)) { id(espcam).set_test_pattern(value); state_return = true; }
if (true == state_return) {
id(espcam).update_camera_parameters();
}
else {
ESP_LOGW("esp32_camera_set_param", "Error in name or data range");
}
ota:
password: !secret kitchen_espcam_ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
manual_ip:
static_ip: 192.168.1.x
gateway: 192.168.1.x
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Kitchen-Espcam Fallback Hotspot"
password: !secret kitchen_espcam_eap_password
captive_portal:
esp32_camera_web_server:
- port: 8080
mode: stream
- port: 8081
mode: snapshot
esp32_camera:
id: espcam
name: esp-cam
external_clock:
pin: GPIO0
frequency: 10MHz
i2c_pins:
sda: GPIO26
scl: GPIO27
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 800x600
jpeg_quality: 24 # max. 63
max_framerate: 4.0fps
idle_framerate: 0.2fps
vertical_flip: false
horizontal_mirror: false
brightness: 2 # -2 to 2
contrast: 1 # -2 to 2
special_effect: none
# exposure settings
aec_mode: auto
aec2: false
ae_level: 0
aec_value: 300
# gain settings
agc_mode: auto
agc_gain_ceiling: 2x
agc_value: 0
# white balance setting
wb_mode: auto
output:
# white LED
- platform: ledc
channel: 2
pin: GPIO4
id: espCamLED
# red status light
- platform: gpio
pin:
number: GPIO33
inverted: True
id: gpio_33
light:
- platform: monochromatic
output: espCamLED
name: esp-cam light
- platform: binary
output: gpio_33
name: esp-cam led
switch:
- platform: restart
name: esp-cam restart
binary_sensor:
- platform: status
name: esp-cam status
sensor:
- platform: uptime
name: "ESP32 Camera Uptime Sensor"
- platform: wifi_signal
name: "ESP32 Camera WiFi Signal"
update_interval: 60s
While the boards do seem to heat up somewhat, they don’t get excessively hot and continue to operate within the required parameters. The main thing is to not keep the flash light on for too long.
Now most things seem to expect the output as an RTSP h264 format, which would be a little too much processing for the little ESP device, so instead I use a self-hosted service that takes in the MJpeg stream and transcodes it to h264. This also allows it to act as a proxy, so clients just connect to the service rather than directly to the ESP. This service is Go2RTC; it internally uses ffmpeg, so you can provide it with custom arguments if required. This is needed for the ESP device as its framerate can change quite a bit, so you will need to tell ffmpeg to ignore any missing frames, etc. The configuration for this can be found here:
streams:
kitchen:
- http://192.168.1.x:8080
- ffmpeg:kitchen#video=h264#hardware=vaapi#raw=-avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -use_wallclock_as_timestamps 1
Lastly, there is a way to view the stream and detect things happening in it. For this, I am using Frigate NVR, which uses AI to detect objects, and then getting this into Home Assistant to automate notifications and other things. Much like with Go2RTC, I set ffmpeg to expect a low-quality stream just to be sure that if anything happens, Frigate will still receive some data. The resolution and fps should match what you have set in the EspHome config; the full config for it can be found here:
kitchen:
ffmpeg:
output_args:
record: preset-record-generic
inputs:
- path: rtsp://go2rtc:8554/kitchen
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -use_wallclock_as_timestamps 1
roles:
- detect
- record
- snapshot
rtmp:
enabled: False
detect:
enabled: True
width: 800
height: 600
fps: 4
record:
enabled: True
snapshots:
enabled: True
timestamp: True
This all finally sets up Frigate with a camera feed, and then you can use the integrations to hook it up to Home Assistant in order to, for example, turn lights on or receive a notification if someone is detected while you are out of the house.