This articles covers a simple camera server configuration for streaming and snapshot capture for use with Home Assistant & ESPHome.
Table of Contents
Hardware used:
- Firebeetle 2 Board ESP32-S3(N16R8)
Overview
The board used by this configuration was: Firebeetle 2 Board ESP32-S3(N16R8)
The board selection used under ESPHome is: dfrobot_firebeetle2_esp32s3
and requires the use of a custom component: AXP313A
which is a power managemenr controller; it is used to control power to the camera during boot.
How the ESPHome configuration works with the AXP313A
custom component to enable power:
- The
i2c
bus is scanned fordfrobot_axp313a
with prority 900. - Under
esphome
theon_boot
events runsid(my_axp313a).setup();
with priority 800 afterdfrobot_axp313a
has been powered. esp32_camera
&esp32_camera_web_server
both usesetup_priority
value -100.0 meaning after everything else has started; this is to ensure that the camera is powered and ready.
Using the custom component
From GitHub
external_components:
- source:
type: git
url: https://github.com/mortanius-1/DFRobot-AXP313A
ref: main
components: [ dfrobot_axp313a ]
Manual usage
Copy the <root>/esphome/*
contents to your esphome
configuration path within HAOS for example: homeassistant/esphome/*
.
For example you should end up with: /homeassistant/esphome/components/dfrobot_axp313a
containing 3 files:
__init__.py
dfrobot_axp313a.cpp
dfrobot_axp313a.h
Then combinations of:
# This refences the components/ path we created
external_components:
- source: components
# This refers to components/dfrobot_axp313a with __init__.py used to load our cpp code.
dfrobot_axp313a:
id: my_axp313a
i2c_id: bus_a
setup_priority: 900.0
External references
- Firebeetle 2 Board ESP32-S3(N16R8) – Wiki
- DFRobot AXP313A – GitHub
- The custom component created for this project was derived from the
esp_idf
code in the DFRobots AXP313A Powermanagement Driver linked in their wiki.
- The custom component created for this project was derived from the
- Review details on priorities and setup_priorities.
YAML Source
substitutions:
id:
name:
friendly_name: Camera
frame_rate_buffer_size: "10"
resolution: 1024x768 # 1600x1200 # FRAMESIZE_UXGA
jpeg_quality: "12"
max_framerate: 10.0fps
idle_framerate: 1.0fps # 0.05fps
vertical_flip: "false"
horizontal_mirror: "false"
brightness: "2"
special_effect: none
aec_mode: auto
aec2: "false"
ae_level: "0"
aec_value: "300"
agc_mode: auto
agc_gain_ceiling: 2x
agc_value: "0"
wb_mode: auto
contrast: "2"
saturation: "-2"
esphome:
project:
name: "oddineers.cameras"
version: "0.2.0"
name: ${name}
friendly_name: ${friendly_name}
libraries:
- Wire
on_boot:
priority: 800
then:
# Could also call: // enableCameraPower(0); // 0 for OV2640, 1 for OV7725 note that setup() provide a delay after powering up camera
- lambda: |-
id(my_axp313a).setup();
psram:
mode: octal
speed: 80MHz
esp32:
board: dfrobot_firebeetle2_esp32s3
framework:
type: arduino
version: recommended
flash_size: 16MB
# If you copy the `dfrobot_axp313a` component files locally use:
# external_components:
# - source: components
external_components:
- source:
type: git
url: https://github.com/mortanius-1/DFRobot-AXP313A
ref: main
components: [ dfrobot_axp313a ]
dfrobot_axp313a:
id: my_axp313a
i2c_id: bus_a
setup_priority: 900.0
i2c:
- id: bus_a
sda: GPIO1
scl: GPIO2
scan: True
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
#domain: .local
# Powersaving ((if you have issue condider commenting out/uncommenting)
power_save_mode: none
#output_power: 8.5db
fast_connect: True
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: " Fallback Hotspot"
password: !secret ap_password
captive_portal:
logger:
level: debug
logs:
esp32_camera: DEBUG
button:
- platform: restart
name: "Cam Restart"
#internal: true
id: restart_cam
# Firebeetle 2 Board ESP32-S3(N16R8) Camera configuration
esp32_camera:
id: ${id}
name: "Camera"
setup_priority: -100.0
external_clock:
pin: GPIO45 # XMCLK
frequency: 20MHz
i2c_id: bus_a
#i2c_pins:
# sda: GPIO1
# scl: GPIO2
data_pins: [GPIO39, GPIO40, GPIO41, GPIO4, GPIO7, GPIO8, GPIO46, GPIO48]
vsync_pin: GPIO6
href_pin: GPIO42
pixel_clock_pin: GPIO5
#reset_pin: GPIO38
resolution: ${resolution}
jpeg_quality: ${jpeg_quality}
max_framerate: ${max_framerate}
idle_framerate: ${idle_framerate}
frame_buffer_count: 2
vertical_flip: ${vertical_flip}
horizontal_mirror: ${horizontal_mirror}
brightness: ${brightness}
contrast: ${contrast}
saturation: ${saturation}
special_effect: ${special_effect}
aec_mode: ${aec_mode}
aec2: ${aec2}
ae_level: ${ae_level}
aec_value: ${aec_value}
agc_mode: ${agc_mode}
agc_gain_ceiling: ${agc_gain_ceiling}
agc_value: ${agc_value}
wb_mode: ${wb_mode}
# Camera web server
esp32_camera_web_server:
- port: 8080
mode: stream
setup_priority: -100.0
- port: 8081
mode: snapshot
setup_priority: -100.0
binary_sensor:
- platform: status
name: Camera status
# Sensors for Home Assistant
sensor:
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
- platform: uptime
name: "Uptime"
update_interval: 110s
text_sensor:
- platform: wifi_info
ip_address:
name: "IP"
ssid:
name: "SSID"
bssid:
name: "BSSID"
mac_address:
name: "MAC"
dns_address:
name: "DNS"
# Enable time component for timestamp
time:
- platform: homeassistant
id: homeassistant_time
# Enable Home Assistant API
api:
encryption:
key: !secret api_key
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(${id}).set_contrast(value); state_return = true; }
if (("brightness" == name) && (value >= -2) && (value <= 2)) { id(${id}).set_brightness(value); state_return = true; }
if (("saturation" == name) && (value >= -2) && (value <= 2)) { id(${id}).set_saturation(value); state_return = true; }
if (("special_effect" == name) && (value >= 0U) && (value <= 6U)) { id(${id}).set_special_effect((esphome::esp32_camera::ESP32SpecialEffect)value); state_return = true; }
if (("aec_mode" == name) && (value >= 0U) && (value <= 1U)) { id(${id}).set_aec_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
if (("aec2" == name) && (value >= 0U) && (value <= 1U)) { id(${id}).set_aec2(value); state_return = true; }
if (("ae_level" == name) && (value >= -2) && (value <= 2)) { id(${id}).set_ae_level(value); state_return = true; }
if (("aec_value" == name) && (value >= 0U) && (value <= 1200U)) { id(${id}).set_aec_value(value); state_return = true; }
if (("agc_mode" == name) && (value >= 0U) && (value <= 1U)) { id(${id}).set_agc_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
if (("agc_value" == name) && (value >= 0U) && (value <= 30U)) { id(${id}).set_agc_value(value); state_return = true; }
if (("agc_gain_ceiling" == name) && (value >= 0U) && (value <= 6U)) { id(${id}).set_agc_gain_ceiling((esphome::esp32_camera::ESP32AgcGainCeiling)value); state_return = true; }
if (("wb_mode" == name) && (value >= 0U) && (value <= 4U)) { id(${id}).set_wb_mode((esphome::esp32_camera::ESP32WhiteBalanceMode)value); state_return = true; }
if (("test_pattern" == name) && (value >= 0U) && (value <= 1U)) { id(${id}).set_test_pattern(value); state_return = true; }
if (true == state_return) {
id(${id}).update_camera_parameters();
}
else {
ESP_LOGW("esp32_camera_set_param", "Error in name or data range");
}
# Camera Controls
number:
- platform: template
name: "Camera Brightness"
id: camera_brightness
min_value: -2
max_value: 2
step: 0.1
initial_value: 0
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${id}).set_brightness(x);
ESP_LOGD("camera", "Brightness set to: %.1f", x);
- platform: template
name: "Camera Contrast"
id: camera_contrast
min_value: -2
max_value: 2
step: 0.1
initial_value: 0
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${id}).set_contrast(x);
ESP_LOGD("camera", "Contrast set to: %.1f", x);
- platform: template
name: "Camera Saturation"
id: camera_saturation
min_value: -2
max_value: 2
step: 0.1
initial_value: 0
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${id}).set_saturation(x);
ESP_LOGD("camera", "Saturation set to: %.1f", x);
- platform: template
name: "Image Quality"
id: image_quality
min_value: 1
max_value: 63
step: 1
initial_value: 12
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${id}).set_jpeg_quality(x);
ESP_LOGD("camera", "Image Quality set to: %d", (int) x);
- platform: template
name: "AEC2"
id: aec2_enabled
min_value: 0
max_value: 1
step: 1
initial_value: 0
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${id}).set_aec2(x);
ESP_LOGD("camera", "AEC2 set to: %d", (int) x);