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

descriptionStandard

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

Hardware used:

  • Espressif ESP32-S3-EYE v2.2

Overview

This configuration provides a simple streaming/snapshotcamera server configuration for use with ESPHome.

Hardware used:

  • Espressif ESP32-S3-EYE v2.2

Assembly summary

No wiring needed; pre-assembled.

Customisations

In the configuration you will need to change/provide values for:

  • Device name, replace <device-name>
  • Secret: api_key
  • Secret: ota_password
  • Secret: wifi_ssid
  • Secret: wifi_password
  • Secret: ap_password

Preparations for flashing the firmware

  1. Note: you may need perform taken from the docs: Hold down BOOT button and press RST button, then release RST button first, and BOOT button next. In this way, the board enters Firmware Download mode and you can start flashing program onto the board. After flashing the program, press RST button to start the program.
  2. Identify the TTL device (typically dev/ttyACM0 or sometimes /dev/ttyUSB0):
ls /dev/tty*

Accessing a tty device may require additional permissions add with (check this appropriate before blindly running):

sudo usermod -a -G dialout $USER

Flash with esptool.py

For example, note the ESP32-S3-EYE flash size is 8MB:

esptool.py --port /dev/ttyACM0 --baud 115200 write_flash -fm dio -fs 8MB 0x0 /path/yo/your/firmware.bin

Notes

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}
  platformio_options:
    build_flags: "-DBOARD_HAS_PSRAM"
    board_build.arduino.memory_type: qio_opi

esp32:   
  variant: esp32s3
  board: esp32s3camlcd
  framework:
    type: arduino
  flash_size: 8MB

# TODO: Note the LCD is powered but unused need to add something to power the LCD down as not used
# In the meantime just disconnect it.

psram:
  mode: octal
  speed: 80MHz

# 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(odd_cam).set_contrast(value); state_return = true; }
          if (("brightness" == name) && (value >= -2) && (value <= 2)) { id(odd_cam).set_brightness(value); state_return = true; }
          if (("saturation" == name) && (value >= -2) && (value <= 2)) { id(odd_cam).set_saturation(value); state_return = true; }
          if (("special_effect" == name) && (value >= 0U) && (value <= 6U)) { id(odd_cam).set_special_effect((esphome::esp32_camera::ESP32SpecialEffect)value); state_return = true; }
          if (("aec_mode" == name) && (value >= 0U) && (value <= 1U)) { id(odd_cam).set_aec_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
          if (("aec2" == name) && (value >= 0U) && (value <= 1U)) { id(odd_cam).set_aec2(value); state_return = true; }
          if (("ae_level" == name) && (value >= -2) && (value <= 2)) { id(odd_cam).set_ae_level(value); state_return = true; }
          if (("aec_value" == name) && (value >= 0U) && (value <= 1200U)) { id(odd_cam).set_aec_value(value); state_return = true; }
          if (("agc_mode" == name) && (value >= 0U) && (value <= 1U)) { id(odd_cam).set_agc_mode((esphome::esp32_camera::ESP32GainControlMode)value); state_return = true; }
          if (("agc_value" == name) && (value >= 0U) && (value <= 30U)) { id(odd_cam).set_agc_value(value); state_return = true; }
          if (("agc_gain_ceiling" == name) && (value >= 0U) && (value <= 6U)) { id(odd_cam).set_agc_gain_ceiling((esphome::esp32_camera::ESP32AgcGainCeiling)value); state_return = true; }
          if (("wb_mode" == name) && (value >= 0U) && (value <= 4U)) { id(odd_cam).set_wb_mode((esphome::esp32_camera::ESP32WhiteBalanceMode)value); state_return = true; }
          if (("test_pattern" == name) && (value >= 0U) && (value <= 1U)) { id(odd_cam).set_test_pattern(value); state_return = true; }
          if (true == state_return) {
            id(odd_cam).update_camera_parameters();
          }
          else {
            ESP_LOGW("esp32_camera_set_param", "Error in name or data range");
          }

ota:
  - platform: esphome
    password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # If you don't use .local for your IOT devices change that here
  #domain: .local

  # Powersaving
  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

i2c:
  - id: bus_a
    sda: GPIO4
    scl: GPIO5
    scan: True

# ESP32-S3 Eye Camera configuration
# Cam clk freq above 8Mhz MAY lock up the esp32-s3-eye device: https://github.com/esphome/issues/issues/4191
esp32_camera:
  id: odd_cam
  name: "Camera"
  external_clock:
    pin: GPIO15
    frequency: 20MHz
  i2c_id: bus_a
  data_pins: [GPIO11, GPIO9, GPIO8, GPIO10, GPIO12, GPIO18, GPIO17, GPIO16]
  vsync_pin: GPIO6
  href_pin: GPIO7
  pixel_clock_pin: GPIO13
  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: 80
    mode: stream
  - port: 81
    mode: snapshot
  
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"

# Button to restart
button:
  - platform: restart
    name: "Camera Restart"

# Enable time component for timestamp
time:
  - platform: homeassistant
    id: homeassistant_time

# 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 *