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

Leave a Reply

I'm pleased you have chosen to leave a comment and engage in a meaningful conversation. Please keep in mind that comments are moderated according to our privacy policy, links are set to nofollow.

Your email address will not be published. Required fields are marked *