Seeed Xaio ESP32S3 with OV5640 camera - success

I was able to get this device working and integrated with HomeAssistant. Sharing to hopefully help save others some time…

substitutions:
  name: esphome-web-123456
  friendly_name: Seeed Xaio w/Camera 123456

  # Pin definitions
  camera_sda_pin: GPIO40
  camera_scl_pin: GPIO39
  camera_data_pins: GPIO15, GPIO17, GPIO18, GPIO16, GPIO14, GPIO12, GPIO11, GPIO48
  camera_vsync_pin: GPIO38
  camera_href_pin: GPIO47
  camera_pclk_pin: GPIO13
  camera_xclk_pin: GPIO10  # Assign the appropriate GPIO pin for xclk

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  platformio_options:
  #  board_build.flash_mode: dio
    board_build.mcu: esp32s3
    build_flags: -DBOARD_HAS_PSRAM
    board_build.arduino.memory_type: qio_opi
    board_build.f_flash: 80000000L
    board_build.flash_mode: qio 
 # https://wiki.seeedstudio.com/XIAO_ESP32S3_esphome/

  project:
    name: esphome.web
    version: '1.0'

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
    #version: latest

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  
api:
  encryption:
    key: <your api key here>

ota:              # Allow Over-The-Air updates
  - platform: esphome

logger: 

web_server:
    
sensor:
  - platform: uptime
    name: "Uptime"
    id: esp_uptime
    icon: mdi:clock-start
    entity_category: "diagnostic"
    update_interval: 60s    
  - platform: uptime
    name: "ESP32 Camera Uptime Sensor"
    id: foo_uptime
    entity_category: "diagnostic"
    icon: mdi:information
  - platform: wifi_signal
    name: "ESP32 Camera WiFi Signal"
    id: foo_wifi
    entity_category: "diagnostic"
    icon: mdi:information
    update_interval: 60s    

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "Wifi IP Address"
      id: wifi_ip_address
      entity_category: "diagnostic"
    ssid:
      name: "WiFi SSID"
      id: wifi_ssid
      icon: mdi:ip-outline
      entity_category: diagnostic
    mac_address:
      name: "WiFi MAC Address"
      id: wifi_mac_address
      icon: mdi:ip-outline
      entity_category: diagnostic  
 
# Camera configuration
esp32_camera:
  name: "Seeed XIAO ESP32-S3 Camera"
  id: seeed_camera
  external_clock: 
    pin: ${camera_xclk_pin}
    frequency: 12MHz
  i2c_pins:
    sda: ${camera_sda_pin}
    scl: ${camera_scl_pin}
  data_pins: 
    - GPIO15
    - GPIO17
    - GPIO18
    - GPIO16
    - GPIO14
    - GPIO12
    - GPIO11
    - GPIO48
  vsync_pin: ${camera_vsync_pin}
  href_pin: ${camera_href_pin}
  pixel_clock_pin: ${camera_pclk_pin}
  resolution: 1280X1024
  jpeg_quality: 10
  max_framerate: 15 fps

switch:
  - platform: template
    name: "Camera Control"
    id: camera_control
    optimistic: True 
  - platform: restart
    id: foo_restart
    name: "ESP32 Camera Restart"    
1 Like

My rudimentary lovelace card

- type: sections
    max_columns: 4
    icon: mdi:camera
    path: seeed-w-camera
    title: Seeed w Camera
    sections:
      - type: grid
        cards:
          - type: heading
            heading: New section
          - type: tile
            entity: sensor.esphome_web_20c368_wifi_ip_address
            name: IP Address
          - type: tile
            entity: sensor.esphome_web_20c368_wifi_mac_address
            name: MAC Address
          - type: tile
            entity: sensor.esphome_web_20c368_wifi_ssid
            name: Wifi SSID
          - type: tile
            entity: sensor.esphome_web_20c368_esp32_camera_wifi_signal
            name: Wifi Signal
            icon: mdi:ip-outline
          - type: tile
            entity: sensor.esphome_web_20c368_uptime
            name: Uptime (s)
          - type: tile
            entity: button.esphome_web_20c368_restart
            name: Restart
      - type: grid
        cards:
          - type: heading
            heading: New section
          - type: tile
            entity: camera.esphome_web_20c368_seeed_xiao_esp32_s3_camera
            show_entity_picture: true
          - camera_view: live
            type: picture-glance
            title: SecretAgent
            image: https://demo.home-assistant.io/stub_config/kitchen.png
            entities:
              - binary_sensor.remote_ui
              - sensor.sun_next_dawn
            camera_image: camera.esphome_web_20c368_seeed_xiao_esp32_s3_camera
1 Like

Hi
Did you use a general ov5640 camera or the xiao ov5640 ?
I tried to connect a general ov5640 and get error

E (17) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
E (38) gdma: gdma_disconnect(314): no peripheral is connected to the channel
Camera init failed with error 0x105

Thanks Doron

latest updates

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  platformio_options:
    board_build.mcu: esp32s3
    build_flags: -DBOARD_HAS_PSRAM
    board_build.arduino.memory_type: qio_opi
    board_build.f_flash: 80000000L
    board_build.flash_mode: qio 
 # https://wiki.seeedstudio.com/XIAO_ESP32S3_esphome/

  project:
    name: esphome.web
    version: '1.0'

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
    version: latest

substitutions:
  name: esphome-web-20c368
  friendly_name: Seeed Xaio w/Camera 20C368
  # Pin definitions
  camera_sda_pin: GPIO40
  camera_scl_pin: GPIO39
  camera_data_pins: GPIO15, GPIO17, GPIO18, GPIO16, GPIO14, GPIO12, GPIO11, GPIO48
  camera_vsync_pin: GPIO38
  camera_href_pin: GPIO47
  camera_pclk_pin: GPIO13
  camera_xclk_pin: GPIO10  #External clock
  camera_power_pin: GPIO1 #use to turn camera on an off?
  user_led_pin: GPIO21
  camera_max_framerate: '15fps'
  camera_jpeg_quality: '10'
  camera_vertical_flip: 'true'
  camera_horizontal_mirror: 'true'
  camera_contrast: '0'
  camera_brightness: '0'
  camera_saturation: '0'
  camera_special_effect: 'none'
  camera_aec_mode: 'auto'
  camera_aec2: 'false'
  camera_ae_level: '0'
  camera_aec_value: '300'
  camera_agc_mode: 'auto'
  camera_agc_value: '0'
  camera_agc_ceiling: '2x'
  camera_white_balance: 'auto'

# Camera configuration
esp32_camera:
  name: "Seeed XIAO ESP32-S3 Camera"
  id: seeed_camera
  #connection options
  data_pins:  
    - GPIO15
    - GPIO17
    - GPIO18
    - GPIO16
    - GPIO14
    - GPIO12
    - GPIO11
    - GPIO48
  vsync_pin: ${camera_vsync_pin}
  href_pin: ${camera_href_pin}
  pixel_clock_pin: ${camera_pclk_pin}
  external_clock: 
    pin: ${camera_xclk_pin}
    frequency: 12MHz
  i2c_pins:
    sda: ${camera_sda_pin}
    scl: ${camera_scl_pin}
  #power_down_pin: !!! done by software switch GPIO1

  #frame settings  
  max_framerate: ${camera_max_framerate}
  idle_framerate: 0.1fps #default
  
  #Image Settings
  resolution: 1280X1024
  jpeg_quality: ${camera_jpeg_quality}
  vertical_flip: ${camera_vertical_flip}
  horizontal_mirror: ${camera_horizontal_mirror}
  brightness: ${camera_brightness}
  contrast: ${camera_contrast}
  saturation: ${camera_saturation}
  special_effect: ${camera_special_effect}

  #Exposure settings
  aec_mode: ${camera_aec_mode}
  aec2: ${camera_aec2}
  ae_level: ${camera_ae_level}
  aec_value: ${camera_ae_level}
  
  #Sensor gain settings
  agc_mode: ${camera_aec_mode}
  agc_gain_ceiling: ${camera_agc_ceiling}
  agc_value: ${camera_agc_value}  
  
  #White balance settings
  wb_mode: ${camera_white_balance}
  
web_server:
  
packages:
  device_base: !include soup_device_base.yaml
  camera_config: !include z_config_ov5640_camera.yaml 
    
switch:
  - platform: template
    name: "Camera Control"
    id: camera_control_template
    optimistic: True
    turn_on_action: 
      then:
        - switch.turn_on: camera_power_output
    turn_off_action: 
      then:
        - switch.turn_off: camera_power_output    
  
  - platform: restart
    id: seecd_restart
    name: "Camera Restart" 
  
  - platform: gpio
    pin: ${camera_power_pin}
    id: camera_power_output
    name: "camera output"

  - platform: template
    name: "Reset Camera"
    id: reset_camera_settings
    turn_on_action: # Reset values to default
      - lambda: |-
          // Set camera to default values using substitutions and proper conversions
          id(seeed_camera).set_contrast(0);
          id(seeed_camera).set_brightness(0);     
      - switch.turn_off: reset_camera_settings  # Ensure the switch is momentary
           
light:
  - platform: monochromatic
    output: onboard_user_led
    name: "Onboard LED"
    id: onboard_led

output:
  - platform: ledc
    pin: ${user_led_pin}
    id: onboard_user_led
    frequency: 5000
    inverted: true #aligns switch and LED 

sensor: 
  - platform: homeassistant
    id: camera_jpeg_quality
    entity_id: input_number.camera_jpeg_quality

  - platform: homeassistant
    id: camera_vertical_flip
    entity_id: input_boolean.camera_vertical_flip

  - platform: homeassistant
    id: camera_horizontal_mirror
    entity_id: input_boolean.camera_horizontal_mirror

  - platform: homeassistant
    id: camera_brightness
    entity_id: input_number.camera_brightness

  - platform: homeassistant
    id: camera_contrast
    entity_id: input_number.camera_contrast

  - platform: homeassistant
    id: camera_saturation
    entity_id: input_number.camera_saturation

  - platform: homeassistant
    id: camera_special_effect
    entity_id: input_select.camera_special_effect

  - platform: homeassistant
    id: camera_aec_mode
    entity_id: input_select.camera_aec_mode

  - platform: homeassistant
    id: camera_aec2
    entity_id: input_boolean.camera_aec2

  - platform: homeassistant
    id: camera_ae_level
    entity_id: input_number.camera_ae_level

  - platform: homeassistant
    id: camera_ae_value
    entity_id: input_number.camera_ae_value 
   
  - platform: homeassistant
    id: camera_agc_mode
    entity_id: input_select.camera_agc_mode

  - platform: homeassistant
    id: camera_agc_ceiling
    entity_id: input_number.camera_agc_ceiling

  - platform: homeassistant
    id: camera_agc_value
    entity_id: input_number.camera_agc_value

  - platform: homeassistant
    id: camera_white_balance
    entity_id: input_select.camera_white_balance       

1 Like

Hi Steve, I’m trying to follow your solution but without success. I always receive an error : [esp32_camera:199]: Got invalid frame from camera! I have the same your hardware. Could you help me?

Thanks for this post it saved me a lot of time, messing with GPIO settings. However if I may ask a couple of questions please"
I have Seeed Studio XIAO ESP32S3 Sense with a OV5640.
I does NOT have an external antenna.
It runs streams the image painfully slow, I get one image every 10 seconds, I’m I just expecting to much from this device? Has anyone had a smooth video stream?
Many Thanks for any suggestions of feedback

I’m not really using it in ‘production’. It seemed slow overall… antenna may help?

I’m attempting to get this to work using your latest updates posted on June 29th. When I go to compile I get the error, “Error reading file /config/esphome/soup_device_base.yaml: [Errno 2] No such file or directory: ‘/config/esphome/soup_device_base.yaml’”

I’m assuming these files are needed in order to use the code:

packages:
  device_base: !include soup_device_base.yaml
  camera_config: !include z_config_ov5640_camera.yaml 

I’ve been offline with some home/family stuff. If it’s the same exact hardware - not sure why my provided code wouldn’t work. What do you get or see?

soup_base is a file I include in all my esps… just standard stuff for ESP diagnostics

type or paste code here

# Enable Home Assistant API
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
   
api:
  id: api_connected
  encryption:
    key: 1<your key here>
  on_client_connected:
      then:
      - logger.log: "Wi-Fi connected"
      - component.update: wifi_ip_address
  

ota:              # Allow Over-The-Air updates
  - platform: esphome

logger: 

### SENSORS
sensor:
  - platform: uptime
    name: "Uptime"
    id: esp_uptime
    accuracy_decimals: 0
    icon: mdi:clock-start
    update_interval: 60s
    entity_category: diagnostic
    internal: true
    filters:
      - delta: 60  # only push if uptime changes by 60s

  - platform: internal_temperature
    name: "ESP Onboard Temp (°C)"
    id: esp_onboard_temp_c
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    icon: mdi:temperature-celsius
    update_interval: 600s
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    on_value:
      then:
        - component.update: esp_onboard_temp_f

  - platform: template
    name: "ESP Onboard Temp (°F)"
    id: esp_onboard_temp_f
    accuracy_decimals: 1
    icon: mdi:temperature-fahrenheit
    unit_of_measurement: "°F"
    update_interval: never  # [derived from °C]
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    lambda: |-
      if (!id(esp_onboard_temp_c).has_state()) return NAN;
      return id(esp_onboard_temp_c).state * 9.0 / 5.0 + 32.0;

  - platform: wifi_signal
    name: "WiFi Signal RSSI"
    id: wifi_signal_db
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    update_interval: 600s
    device_class: signal_strength
    entity_category: diagnostic
    filters:
      - delta: 2  # only push if RSSI changes by 2 dBm

  - platform: copy
    source_id: wifi_signal_db
    name: "WiFi Signal %"
    id: wifi_signal_pct
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    unit_of_measurement: "%"
    entity_category: diagnostic
    device_class: signal_strength
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
      - delta: 2
    ### device_class: ""  # 👈 Prevents Home Assistant from validating unit
    
### TEXT SENSORS
text_sensor:
  - platform: template
    name: "ESP Uptime (DHM)"
    id: uptime_dhm
    icon: mdi:clock-outline
    entity_category: diagnostic
    update_interval: 60s  # refresh every minute
    lambda: |-
      uint32_t uptime_sec = (uint32_t)id(esp_uptime).state;
      uint32_t days = uptime_sec / 86400;
      uint32_t hours = (uptime_sec % 86400) / 3600;
      uint32_t minutes = (uptime_sec % 3600) / 60;

      char buffer[32];
      snprintf(buffer, sizeof(buffer), "%ud %02uh %02um", days, hours, minutes);
      return std::string(buffer);

  - platform: version
    name: "ESP Firmware Version"
    id: esp_firmware_version
    icon: mdi:information-variant
    # update_interval: [static value]
    entity_category: diagnostic
    hide_timestamp: true

  - platform: template
    name: "ESP WiFi IP Address"
    id: wifi_ip_address
    update_interval: 120s
    icon: mdi:ip-outline
    entity_category: diagnostic
    lambda: |-
      static std::string last_ip;
      std::string current_ip = id(wifi_ip_address_raw).state;
      if (current_ip.empty() || current_ip == last_ip) return {};
      last_ip = current_ip;
      return current_ip;

  - platform: wifi_info
    ip_address:
      name: "WiFi IP Address (raw)"
      id: wifi_ip_address_raw
      icon: mdi:ip-outline
      entity_category: diagnostic
      internal: True
      update_interval: 60s
    ssid:
      name: "WiFi SSID"
      id: wifi_ssid
      icon: mdi:ip-outline
      # update_interval: [event-driven]
      entity_category: diagnostic
    mac_address:
      name: "WiFi MAC Address"
      id: wifi_mac_address
      icon: mdi:ip-outline
      # update_interval: [event-driven]
      entity_category: diagnostic

  - platform: template
    name: "WiFi MAC Address (compact)"
    id: wifi_mac_address_stripped
    icon: mdi:ip-outline
    update_interval: 600s  
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac;

  - platform: template
    name: "WiFi MAC Address [6]"
    id: wifi_mac_address_last_6
    icon: mdi:ip-outline
    update_interval: 600s  
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac.substr(stripped_mac.length() - 6);

#### BINARY SENSORS
binary_sensor:
  - platform: status
    name: "ESP HA Connection Status"
    id: esp_ha_connected_status
    icon: mdi:cloud-check
    device_class: connectivity
    entity_category: diagnostic

### SOFTWARE BUTTONS
button:
  - platform: restart
    name: "ESP Restart"
    icon: mdi:power-cycle
    entity_category: diagnostic
    on_press:
      then:
        - logger.log:
            format: "Restart triggered on ESP [${friendly_name}]"         

here’s the z_config ov5640 file… sorry about forgetting to include it

input_number:
  camera_jpeg_quality:
    name: "JPEG Quality"
    min: 10
    max: 63
    step: 1
    
  camera_max_framerate:
    name: "Max Framerate"
    min: 10
    max: 30
    step: 1
    
  camera_idle_framerate:
    name: "Idle Framerate"
    min: 0.1
    max: 30
    step: 1
    
  camera_ae_level:
    name: "AE Level"
    min: -2
    max: 2
    step: 1
    
  camera_aec_level:
    name: "AEC Level"
    min: 0
    max: 1200
    step: 100
    
  camera_agc_level:
    name: "AGC Level"
    min: 0
    max: 30
    step: 1
    
  camera_brightness:
    name: "Brightness"
    min: -2
    max: 2
    step: 1
    
  camera_contrast:
    name: "Contrast"
    min: -2
    max: 2
    step: 1
    
  camera_saturation:
    name: "Saturation"
    min: -2
    max: 2
    step: 1

  camera_quality:
    name: "JPEG Quality"
    min: 10
    max: 63
    step: 1

input_boolean:
  camera_vertical_flip:
    name: "Vertical Flip"

  camera_horizontal_mirror:
    name: "Horizontal Mirror"

  camera_awb_gain:
    name: "AWB Gain"

  camera_aec2:
    name: "AEC2"

input_select:
  camera_aec_mode:
    name: "AEC mode"
    options:
      - "auto"
      - "manual"

  camera_agc_mode:
    name: "AGC mode"
    options:
      - "auto"
      - "manual"

  camera_agc_ceiling:
    name: "AGC Ceiling"
    options:
      - "2x"
      - "4x"
      - "8x"
      - "16x"
      - "32x"
      - "64x"
      - "128x"

  camera_special_effect:
    name: "Special Effect"
    options:
      - "none"
      - "negative"
      - "grayscale"
      - "red_tint"
      - "green_tint"
      - "blue_tint"
      - "sepia"

  camera_white_balance:
    name: "White Balance"
    options:
      - "auto"
      - "sunny"
      - "cloudy"
      - "office"
      - "home"

1 Like

I have a Freenove ESP32-S3 clone + ov5640 (aliexpress) with following config for Homeassistant 2025.12 and latest esphome. But i cant get the homeassistant camera settings to apply to the ov5640. I cant toggle switches, and these show up in the esp32 logs, but e.g. the vertical flip isnt applied to the camera (feed).
I tried to reduce i2c speed to 100kHz, enabled PSRAM to octal (for my s3-N16-R8) and changed default brightnes to 2, because the ov5640 feed is so dark.
But my “speed”, as mentioned in october by steve, is sufficient and i get a smooth video stream.

Can someone please suggest any changes, so my ov5640 is not so dimm (ov2640 and ov2540 are working flawlessly) AND:
make the switches in homeassistant (or esphome ui) apply to the live camera feed?

these are my logs

[esp32_camera:207]: Got Image: len=12125
[number:065]: 'Camera Brightness': Setting value
[number:124]:   New value: 2.000000
[number:035]: 'Camera Brightness': Sending state 2.000000
[esp32_camera:207]: Got Image: len=12125

but these dont seem to apply to the cam.

esphome:
  name: esp32-s3
  friendly_name: esp32-s3

  on_boot:
    priority: 600
    then:
      - light.turn_on:
          id: onboard_led
          brightness: 60%
          red: 0%
          green: 100%
          blue: 0%
      - lambda: id(esp32s3_cam).set_aec_mode(esphome::esp32_camera::ESP32_GC_MODE_AUTO);
      - lambda: id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_AUTO);
      - lambda: id(esp32s3_cam).set_brightness(2);
      - delay: 1s
      - light.turn_off: onboard_led

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

psram:
  mode: octal
  speed: 80MHz

logger:
  level: VERBOSE

api:
  encryption:
    key: "xyz="

ota:
  - platform: esphome
    password: "my_pw"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  domain: .dns.tld

  manual_ip:
    static_ip: 192.168.1.2
    gateway: 192.168.1.1
    subnet: 255.255.255.0
    dns1: 192.168.1.1
    dns2: 8.8.8.8

  ap:
    ssid: "Esp32-S3 Fallback Hotspot"
    password: "my_pw"

captive_portal:

# lowered i2c clock speed
i2c:
  - id: camera_i2c
    sda: GPIO4
    scl: GPIO5
    frequency: 100kHz

# RGB LED
light:
  - platform: esp32_rmt_led_strip
    rgb_order: GRB
    pin: GPIO48
    num_leds: 1
    chipset: WS2812
    name: "Onboard RGB LED"
    id: onboard_led
    restore_mode: RESTORE_DEFAULT_OFF

esp32_camera:
  id: esp32s3_cam
  name: esp32-s3 cam
  
  external_clock:
    pin: GPIO15
    frequency: 20MHz
    
  i2c_id: camera_i2c
  data_pins: [GPIO11, GPIO9, GPIO8, GPIO10, GPIO12, GPIO18, GPIO17, GPIO16]
  vsync_pin: GPIO6
  href_pin: GPIO7
  pixel_clock_pin: GPIO13
  
  # Octal PSRAM is required for the 5MP sensor
  frame_buffer_location: PSRAM

  # Frame settings  
  max_framerate: 15fps 
  # Low idle framerate cools the camera when you aren't looking at it
  idle_framerate: 0.1fps 
  
  # Image Settings
  resolution: 640x480
  jpeg_quality: 10
  vertical_flip: false
  horizontal_mirror: false 
  
  # Defaults, overriden by - on_boot: above
  brightness: 2
  contrast: 0
  saturation: 0
  
  aec_mode: auto
  wb_mode: auto

# Frigate config
web_server:
  port: 80
  include_internal: False

esp32_camera_web_server:
  - port: 8080
    mode: stream
  - port: 8081
    mode: snapshot

# -------------------------
# Sensors
# -------------------------
sensor:
  - platform: uptime
    name: "Uptime"
    id: esp_uptime
    update_interval: 60s
    entity_category: diagnostic
    internal: true

  - platform: internal_temperature
    name: "ESP Onboard Temp (°C)"
    id: esp_onboard_temp_c
    unit_of_measurement: "°C"
    update_interval: 10s
    entity_category: diagnostic

  - platform: wifi_signal
    name: "WiFi Signal RSSI"
    id: wifi_signal_db
    update_interval: 600s
    entity_category: diagnostic

# Homeassistant Controls

number:
  - platform: template
    name: "Camera Contrast"
    id: cam_contrast
    entity_category: config
    icon: mdi:contrast-circle
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    optimistic: true
    set_action:
      - lambda: id(esp32s3_cam).set_contrast(x);

  - platform: template
    name: "Camera Brightness"
    id: cam_brightness
    entity_category: config
    icon: mdi:brightness-6
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    optimistic: true
    set_action:
      - lambda: id(esp32s3_cam).set_brightness(x);

  - platform: template
    name: "Camera Saturation"
    id: cam_saturation
    entity_category: config
    icon: mdi:palette
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    optimistic: true
    set_action:
      - lambda: id(esp32s3_cam).set_saturation(x);

switch:
  - platform: template
    name: "Camera Vertical Flip"
    id: cam_vflip
    entity_category: config
    icon: mdi:flip-vertical
    optimistic: true
    turn_on_action:
      - lambda: id(esp32s3_cam).set_vertical_flip(true);
    turn_off_action:
      - lambda: id(esp32s3_cam).set_vertical_flip(false);

  - platform: template
    name: "Camera Horizontal Mirror"
    id: cam_hmirror
    entity_category: config
    icon: mdi:flip-horizontal
    optimistic: true
    turn_on_action:
      - lambda: id(esp32s3_cam).set_horizontal_mirror(true);
    turn_off_action:
      - lambda: id(esp32s3_cam).set_horizontal_mirror(false);

  - platform: restart
    name: "Restart Camera Board"

select:
  - platform: template
    name: "Camera Effect"
    id: cam_effect
    entity_category: config
    optimistic: true
    options:
      - "None"
      - "Negative"
      - "Grayscale"
      - "Red Tint"
      - "Green Tint"
      - "Blue Tint"
      - "Sepia"
    initial_option: "None"
    set_action:
      - lambda: |-
          if (x == "Negative") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_NEGATIVE);
          else if (x == "Grayscale") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_GRAYSCALE);
          else if (x == "Red Tint") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_RED_TINT);
          else if (x == "Green Tint") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_GREEN_TINT);
          else if (x == "Blue Tint") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_BLUE_TINT);
          else if (x == "Sepia") id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_SEPIA);
          else id(esp32s3_cam).set_special_effect(esphome::esp32_camera::ESP32_SPECIAL_EFFECT_NONE);

  - platform: template
    name: "Camera White Balance"
    id: cam_wb_mode
    entity_category: config
    optimistic: true
    icon: mdi:white-balance-sunny
    options:
      - "Auto"
      - "Sunny"
      - "Cloudy"
      - "Office"
      - "Home"
    initial_option: "Auto"
    set_action:
      - lambda: |-
          if (x == "Sunny") id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_SUNNY);
          else if (x == "Cloudy") id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_CLOUDY);
          else if (x == "Office") id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_OFFICE);
          else if (x == "Home") id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_HOME);
          else id(esp32s3_cam).set_wb_mode(esphome::esp32_camera::ESP32_WB_MODE_AUTO);

Try the code attached, I had a hard time to figure this out with and ChatGPT let me down all the time. In the end, forget the flip switches and only initialized flip or mirror once. I tried my not working code also with an OV5640 and it did not work but as with the init values only, it worked on the OV3660, so I assume it will work on the OV5640 aswell. In an earlier version I even got brightness, contrast and saturation working but in all my hundreds of changes, I broke something. You loose some, you win some

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  platformio_options:
    board_build.mcu: esp32s3
    build_flags: -DBOARD_HAS_PSRAM
    board_build.arduino.memory_type: qio_opi
    board_build.f_flash: 80000000L
    board_build.flash_mode: qio

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

substitutions:
  name: esphome-web-20c368
  friendly_name: Seeed Xaio w/Camera 20C368

  camera_sda_pin: GPIO40
  camera_scl_pin: GPIO39
  camera_vsync_pin: GPIO38
  camera_href_pin: GPIO47
  camera_pclk_pin: GPIO13
  camera_xclk_pin: GPIO10
  camera_power_pin: GPIO1
  user_led_pin: GPIO21

  camera_resolution: 1280X1024

psram:
  mode: octal
  speed: 80MHz

script:
  # Apply all "color-ish" settings together, every time any slider changes
  - id: apply_color_settings
    mode: restart
    then:
      - lambda: |-
          id(seeed_camera).set_brightness((int) id(cam_brightness_num).state);
          id(seeed_camera).set_contrast((int) id(cam_contrast_num).state);
          id(seeed_camera).set_saturation((int) id(cam_saturation_num).state);

  - id: apply_camera_settings
    mode: restart
    then:
      - lambda: |-
          id(seeed_camera).set_jpeg_quality((int) id(cam_jpeg_quality_num).state);
      - script.execute: apply_color_settings

i2c:
- id: camera_i2c
  sda: ${camera_sda_pin}
  scl: ${camera_scl_pin}
  frequency: 100kHz

esp32_camera:
  name: "Seeed XIAO ESP32-S3 Camera"
  id: seeed_camera
  i2c_id: camera_i2c

  data_pins:
    - GPIO15
    - GPIO17
    - GPIO18
    - GPIO16
    - GPIO14
    - GPIO12
    - GPIO11
    - GPIO48

  vsync_pin: ${camera_vsync_pin}
  href_pin: ${camera_href_pin}
  pixel_clock_pin: ${camera_pclk_pin}


  external_clock:
    pin: ${camera_xclk_pin}
    frequency: 20MHz

  # If your wiring supports it, consider letting the component manage PWDN:
  power_down_pin: ${camera_power_pin}

  on_stream_start:
    then:
      - delay: 200ms
      - script.execute: apply_camera_settings

  max_framerate: 15fps
  idle_framerate: 0.1fps

  resolution: ${camera_resolution}
  jpeg_quality: 10

  vertical_flip: true
  horizontal_mirror: false

  brightness: 0
  contrast: 0
  saturation: 0
  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

web_server:
  port: 80

switch:
  - platform: restart
    id: seecd_restart
    name: "Camera Restart"

  - platform: template
    name: "Reset Camera"
    id: reset_camera_settings
    turn_on_action:
      - lambda: |-
          id(cam_brightness_num).publish_state(0);
          id(cam_contrast_num).publish_state(0);
          id(cam_saturation_num).publish_state(0);
          id(cam_jpeg_quality_num).publish_state(10);
      - script.execute: apply_camera_settings
      - switch.turn_off: reset_camera_settings

number:
  - platform: template
    name: "Cam Brightness"
    id: cam_brightness_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam Contrast"
    id: cam_contrast_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam Saturation"
    id: cam_saturation_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam JPEG Quality"
    id: cam_jpeg_quality_num
    min_value: 10
    max_value: 63
    step: 1
    restore_value: true
    initial_value: 10
    set_action:
      - script.execute: apply_camera_settings

light:
  - platform: monochromatic
    output: onboard_user_led
    name: "Onboard LED"
    id: onboard_led

output:
  - platform: ledc
    pin: ${user_led_pin}
    id: onboard_user_led
    frequency: 5000
    inverted: true

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
  encryption:
    key: "your key"

  on_client_connected:
    then:
      - logger.log: "Wi-Fi connected"
      - component.update: wifi_ip_address

ota:
  - platform: esphome

logger:

sensor:
  - platform: uptime
    name: "Uptime"
    id: esp_uptime
    accuracy_decimals: 0
    icon: mdi:clock-start
    update_interval: 60s
    entity_category: diagnostic
    internal: true
    filters:
      - delta: 60

  - platform: internal_temperature
    name: "ESP Onboard Temp (°C)"
    id: esp_onboard_temp_c
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    icon: mdi:temperature-celsius
    update_interval: 600s
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    on_value:
      then:
        - component.update: esp_onboard_temp_f

  - platform: template
    name: "ESP Onboard Temp (°F)"
    id: esp_onboard_temp_f
    accuracy_decimals: 1
    icon: mdi:temperature-fahrenheit
    unit_of_measurement: "°F"
    update_interval: never
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    lambda: |-
      if (!id(esp_onboard_temp_c).has_state()) return NAN;
      return id(esp_onboard_temp_c).state * 9.0 / 5.0 + 32.0;

  - platform: wifi_signal
    name: "WiFi Signal RSSI"
    id: wifi_signal_db
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    update_interval: 600s
    device_class: signal_strength
    entity_category: diagnostic
    filters:
      - delta: 2

  - platform: copy
    source_id: wifi_signal_db
    name: "WiFi Signal %"
    id: wifi_signal_pct
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    unit_of_measurement: "%"
    entity_category: diagnostic
    device_class: signal_strength
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
      - delta: 2

text_sensor:
  - platform: template
    name: "ESP Uptime (DHM)"
    id: uptime_dhm
    icon: mdi:clock-outline
    entity_category: diagnostic
    update_interval: 60s
    lambda: |-
      uint32_t uptime_sec = (uint32_t)id(esp_uptime).state;
      uint32_t days = uptime_sec / 86400;
      uint32_t hours = (uptime_sec % 86400) / 3600;
      uint32_t minutes = (uptime_sec % 3600) / 60;

      char buffer[32];
      snprintf(buffer, sizeof(buffer), "%ud %02uh %02um", days, hours, minutes);
      return std::string(buffer);

  - platform: version
    name: "ESP Firmware Version"
    id: esp_firmware_version
    icon: mdi:information-variant
    entity_category: diagnostic
    hide_timestamp: true

  - platform: template
    name: "ESP WiFi IP Address"
    id: wifi_ip_address
    update_interval: 120s
    icon: mdi:ip-outline
    entity_category: diagnostic
    lambda: |-
      static std::string last_ip;
      std::string current_ip = id(wifi_ip_address_raw).state;
      if (current_ip.empty() || current_ip == last_ip) return {};
      last_ip = current_ip;
      return current_ip;

  - platform: wifi_info
    ip_address:
      name: "WiFi IP Address (raw)"
      id: wifi_ip_address_raw
      icon: mdi:ip-outline
      entity_category: diagnostic
      internal: true

    ssid:
      name: "WiFi SSID"
      id: wifi_ssid
      icon: mdi:ip-outline
      entity_category: diagnostic

    mac_address:
      name: "WiFi MAC Address"
      id: wifi_mac_address
      icon: mdi:ip-outline
      entity_category: diagnostic

  - platform: template
    name: "WiFi MAC Address (compact)"
    id: wifi_mac_address_stripped
    icon: mdi:ip-outline
    update_interval: 600s
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac;

  - platform: template
    name: "WiFi MAC Address [6]"
    id: wifi_mac_address_last_6
    icon: mdi:ip-outline
    update_interval: 600s
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac.substr(stripped_mac.length() - 6);

  - platform: template
    name: "Camera Resolution (configured)"
    id: cam_resolution_configured
    icon: mdi:image-size-select-large
    entity_category: diagnostic
    lambda: |-
      return std::string("${camera_resolution}");

binary_sensor:
  - platform: status
    name: "ESP HA Connection Status"
    id: esp_ha_connected_status
    icon: mdi:cloud-check
    device_class: connectivity
    entity_category: diagnostic

button:
  - platform: restart
    name: "ESP Restart"
    id: esp_restart_btn
    icon: mdi:power-cycle
    entity_category: diagnostic
    on_press:
      then:
        - logger.log:
            format: "Restart triggered on ESP [${friendly_name}]"

Here is the update code, mostly all things are working via the switches except the flip or mirror or resolution, this has to be hardcoded in the yaml file and compiled.

Thank you Steve_Campbell

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  platformio_options:
    board_build.mcu: esp32s3
    build_flags: -DBOARD_HAS_PSRAM
    board_build.arduino.memory_type: qio_opi
    board_build.f_flash: 80000000L
    board_build.flash_mode: qio
  on_boot:
    priority: -100
    then:
      - lambda: |-
          id(g_vflip) = false;
          id(g_hmirror) = true;

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
    version: latest

substitutions:
  name: esphome-web-20c368
  friendly_name: Seeed Xaio w/Camera 20C368

  camera_sda_pin: GPIO40
  camera_scl_pin: GPIO39
  camera_vsync_pin: GPIO38
  camera_href_pin: GPIO47
  camera_pclk_pin: GPIO13
  camera_xclk_pin: GPIO10
  camera_power_pin: GPIO1
  user_led_pin: GPIO21

  camera_resolution: 1280X1024

psram:
  mode: octal
  speed: 80MHz

globals:
  - id: g_vflip
    type: bool
    restore_value: false
    initial_value: "false"

  - id: g_hmirror
    type: bool
    restore_value: true
    initial_value: "true"

script:
  # Apply all "color-ish" settings together, every time any slider changes
  - id: apply_color_settings
    mode: restart
    then:
      - lambda: |-
          id(seeed_camera).set_brightness((int) id(cam_brightness_num).state);
          id(seeed_camera).set_contrast((int) id(cam_contrast_num).state);
          id(seeed_camera).set_saturation((int) id(cam_saturation_num).state);

          const char *wb = id(cam_white_balance).current_option();
          const char *fx = id(cam_effect).current_option();

          using namespace esphome::esp32_camera;

          // WB
          if (strcmp(wb, "sunny") == 0) id(seeed_camera).set_wb_mode(ESP32WhiteBalanceMode::ESP32_WB_MODE_SUNNY);
          else if (strcmp(wb, "cloudy") == 0) id(seeed_camera).set_wb_mode(ESP32WhiteBalanceMode::ESP32_WB_MODE_CLOUDY);
          else if (strcmp(wb, "office") == 0) id(seeed_camera).set_wb_mode(ESP32WhiteBalanceMode::ESP32_WB_MODE_OFFICE);
          else if (strcmp(wb, "home") == 0) id(seeed_camera).set_wb_mode(ESP32WhiteBalanceMode::ESP32_WB_MODE_HOME);
          else id(seeed_camera).set_wb_mode(ESP32WhiteBalanceMode::ESP32_WB_MODE_AUTO);

          // FX
          if (strcmp(fx, "negative") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_NEGATIVE);
          else if (strcmp(fx, "grayscale") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_GRAYSCALE);
          else if (strcmp(fx, "red_tint") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_RED_TINT);
          else if (strcmp(fx, "green_tint") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_GREEN_TINT);
          else if (strcmp(fx, "blue_tint") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_BLUE_TINT);
          else if (strcmp(fx, "sepia") == 0) id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_SEPIA);
          else id(seeed_camera).set_special_effect(ESP32SpecialEffect::ESP32_SPECIAL_EFFECT_NONE);
          ESP_LOGI("cam_apply", "apply_camera_settings() finished");

  - id: apply_camera_settings
    mode: restart
    then:
      - lambda: |-
          id(seeed_camera).set_vertical_flip(id(g_vflip));
          id(seeed_camera).set_horizontal_mirror(id(g_hmirror));
          id(seeed_camera).set_jpeg_quality((int) id(cam_jpeg_quality_num).state);
      - script.execute: apply_color_settings

esp32_camera:
  name: "Seeed XIAO ESP32-S3 Camera"
  id: seeed_camera

  data_pins:
    - GPIO15
    - GPIO17
    - GPIO18
    - GPIO16
    - GPIO14
    - GPIO12
    - GPIO11
    - GPIO48

  vsync_pin: ${camera_vsync_pin}
  href_pin: ${camera_href_pin}
  pixel_clock_pin: ${camera_pclk_pin}

  external_clock:
    pin: ${camera_xclk_pin}
    frequency: 20MHz

  i2c_pins:
    sda: ${camera_sda_pin}
    scl: ${camera_scl_pin}

  # If your wiring supports it, consider letting the component manage PWDN:
  # power_down_pin: ${camera_power_pin}

  on_stream_start:
    then:
      - delay: 200ms
      - script.execute: apply_camera_settings

  max_framerate: 15fps
  idle_framerate: 0.1fps

  resolution: ${camera_resolution}
  jpeg_quality: 10

  vertical_flip: false
  horizontal_mirror: true

  brightness: 0
  contrast: 0
  saturation: 0
  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

web_server:
  port: 80

switch:
  - platform: template
    name: "Reset Camera"
    id: reset_camera_settings
    turn_on_action:
      - lambda: |-
          id(cam_brightness_num).publish_state(0);
          id(cam_contrast_num).publish_state(0);
          id(cam_saturation_num).publish_state(0);
          id(cam_jpeg_quality_num).publish_state(10);
          id(g_vflip) = false;
          id(g_hmirror) = true;
          id(cam_white_balance).publish_state("auto");
          id(cam_effect).publish_state("none");
      - script.execute: apply_camera_settings
      - switch.turn_off: reset_camera_settings

  - platform: template
    name: "Cam Vertical Flip"
    id: cam_vflip_sw
    internal: true
    restore_mode: ALWAYS_OFF
    lambda: |-
      return id(g_vflip);
    turn_on_action:
      - lambda: |-
          id(g_vflip) = true;
      - script.execute: apply_camera_settings
    turn_off_action:
      - lambda: |-
          id(g_vflip) = false;
      - script.execute: apply_camera_settings

  - platform: template
    name: "Cam Horizontal Mirror"
    id: cam_hmirror_sw
    internal: true
    restore_mode: ALWAYS_ON
    lambda: |-
      return id(g_hmirror);
    turn_on_action:
      - lambda: |-
          id(g_hmirror) = true;
      - script.execute: apply_camera_settings
    turn_off_action:
      - lambda: |-
          id(g_hmirror) = false;
      - script.execute: apply_camera_settings

number:
  - platform: template
    name: "Cam Brightness"
    id: cam_brightness_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam Contrast"
    id: cam_contrast_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam Saturation"
    id: cam_saturation_num
    min_value: -2
    max_value: 2
    step: 1
    restore_value: true
    initial_value: 0
    set_action:
      - script.execute: apply_color_settings

  - platform: template
    name: "Cam JPEG Quality"
    id: cam_jpeg_quality_num
    min_value: 10
    max_value: 63
    step: 1
    restore_value: true
    initial_value: 10
    set_action:
      - script.execute: apply_camera_settings

select:
  - platform: template
    name: "Cam White Balance"
    id: cam_white_balance
    optimistic: true
    restore_value: true
    options: [auto, sunny, cloudy, office, home]
    initial_option: auto
    set_action:
      - script.execute: apply_camera_settings

  - platform: template
    name: "Cam Special Effect"
    id: cam_effect
    optimistic: true
    restore_value: true
    options: [none, negative, grayscale, red_tint, green_tint, blue_tint, sepia]
    initial_option: none
    set_action:
      - script.execute: apply_camera_settings

light:
  - platform: monochromatic
    output: onboard_user_led
    name: "Onboard LED"
    id: onboard_led

output:
  - platform: ledc
    pin: ${user_led_pin}
    id: onboard_user_led
    frequency: 5000
    inverted: true

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
  encryption:
    key: your key
  on_client_connected:
    then:
      - logger.log: "Wi-Fi connected"
      - component.update: wifi_ip_address

ota:
  - platform: esphome

logger:

sensor:
  - platform: uptime
    name: "Uptime"
    id: esp_uptime
    accuracy_decimals: 0
    icon: mdi:clock-start
    update_interval: 60s
    entity_category: diagnostic
    internal: true
    filters:
      - delta: 60

  - platform: internal_temperature
    name: "ESP Onboard Temp (°C)"
    id: esp_onboard_temp_c
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    icon: mdi:temperature-celsius
    update_interval: 600s
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    on_value:
      then:
        - component.update: esp_onboard_temp_f

  - platform: template
    name: "ESP Onboard Temp (°F)"
    id: esp_onboard_temp_f
    accuracy_decimals: 1
    icon: mdi:temperature-fahrenheit
    unit_of_measurement: "°F"
    update_interval: never
    entity_category: diagnostic
    device_class: temperature
    state_class: measurement
    lambda: |-
      if (!id(esp_onboard_temp_c).has_state()) return NAN;
      return id(esp_onboard_temp_c).state * 9.0 / 5.0 + 32.0;

  - platform: wifi_signal
    name: "WiFi Signal RSSI"
    id: wifi_signal_db
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    update_interval: 600s
    device_class: signal_strength
    entity_category: diagnostic
    filters:
      - delta: 2

  - platform: copy
    source_id: wifi_signal_db
    name: "WiFi Signal %"
    id: wifi_signal_pct
    accuracy_decimals: 0
    icon: mdi:wifi-strength-outline
    unit_of_measurement: "%"
    entity_category: diagnostic
    device_class: signal_strength
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
      - delta: 2

text_sensor:
  - platform: template
    name: "ESP Uptime (DHM)"
    id: uptime_dhm
    icon: mdi:clock-outline
    entity_category: diagnostic
    update_interval: 60s
    lambda: |-
      uint32_t uptime_sec = (uint32_t)id(esp_uptime).state;
      uint32_t days = uptime_sec / 86400;
      uint32_t hours = (uptime_sec % 86400) / 3600;
      uint32_t minutes = (uptime_sec % 3600) / 60;

      char buffer[32];
      snprintf(buffer, sizeof(buffer), "%ud %02uh %02um", days, hours, minutes);
      return std::string(buffer);

  - platform: version
    name: "ESP Firmware Version"
    id: esp_firmware_version
    icon: mdi:information-variant
    entity_category: diagnostic
    hide_timestamp: true

  - platform: template
    name: "ESP WiFi IP Address"
    id: wifi_ip_address
    update_interval: 120s
    icon: mdi:ip-outline
    entity_category: diagnostic
    lambda: |-
      static std::string last_ip;
      std::string current_ip = id(wifi_ip_address_raw).state;
      if (current_ip.empty() || current_ip == last_ip) return {};
      last_ip = current_ip;
      return current_ip;

  - platform: wifi_info
    ip_address:
      name: "WiFi IP Address (raw)"
      id: wifi_ip_address_raw
      icon: mdi:ip-outline
      entity_category: diagnostic
      internal: true

    ssid:
      name: "WiFi SSID"
      id: wifi_ssid
      icon: mdi:ip-outline
      entity_category: diagnostic

    mac_address:
      name: "WiFi MAC Address"
      id: wifi_mac_address
      icon: mdi:ip-outline
      entity_category: diagnostic

  - platform: template
    name: "WiFi MAC Address (compact)"
    id: wifi_mac_address_stripped
    icon: mdi:ip-outline
    update_interval: 600s
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac;

  - platform: template
    name: "WiFi MAC Address [6]"
    id: wifi_mac_address_last_6
    icon: mdi:ip-outline
    update_interval: 600s
    entity_category: diagnostic
    lambda: |-
      auto mac = id(wifi_mac_address).state;
      std::string stripped_mac;
      for (char c : mac) if (c != ':') stripped_mac += c;
      return stripped_mac.substr(stripped_mac.length() - 6);

  - platform: template
    name: "Camera Resolution (configured)"
    id: cam_resolution_configured
    icon: mdi:image-size-select-large
    entity_category: diagnostic
    lambda: |-
      return std::string("${camera_resolution}");

binary_sensor:
  - platform: status
    name: "ESP HA Connection Status"
    id: esp_ha_connected_status
    icon: mdi:cloud-check
    device_class: connectivity
    entity_category: diagnostic

button:
  - platform: restart
    name: "ESP Restart"
    id: esp_restart_btn
    icon: mdi:power-cycle
    entity_category: diagnostic
    on_press:
      then:
        - logger.log:
            format: "Restart triggered on ESP [${friendly_name}]"

Hey, how about camera’s temperature? got few, and they are extremely hot.