Remote Camera for ESP32-S3-Firebeetle2 – Home Assistant + ESPHome

descriptionStandard

This articles covers a simple camera server configuration for streaming and snapshot capture for use with Home Assistant & ESPHome.

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 for dfrobot_axp313a with prority 900.
  • Under esphome the on_boot events runs id(my_axp313a).setup(); with priority 800 after dfrobot_axp313a has been powered.
  • esp32_camera & esp32_camera_web_server both use setup_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

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);