Waveshare ESP32-S3-ETH PoE OV2640 CAM

@mx738: Toni, how did you configure the LAN PoE for that board? Are you able to kindly share?

I just got the non camera version but POE works fine using the above. You can’t put in WiFi or an AP/captive portal part when using Ethernet. Haven’t had time to add various sensors yet so left the relay in. Just flash it using USB C then plug into POE Ethernet and it shows up to add in HA.

substitutions:
  name: ws-poe-s3
  friendly_name: ws_poe_s3
  device_sampling_time: 30s

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
  flash_size: 16MB

psram:
  mode: octal
  speed: 80MHz  

switch:
  - platform: gpio
    name: ws-poe-32-relay1
    id: relay1                              
    pin: GPIO8
    restore_mode: ALWAYS_OFF
    setup_priority: 800
# Enable logging
logger:

debug:
  update_interval: ${device_sampling_time}   

# Enable Home Assistant API
api:
  encryption:
    key: "Api123="

ota:
  - platform: esphome
    password: "otapw"

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

ethernet:
  type: W5500
  mosi_pin: GPIO11
  miso_pin: GPIO12
  clk_pin: GPIO13
  cs_pin: GPIO14
  reset_pin: GPIO9
  interrupt_pin: GPIO10
  clock_speed: 25MHz  

time:
  - platform: homeassistant
    id: homeassistant_time    

button:
  - platform: safe_mode
    name: Safe Mode Boot

  - platform: factory_reset
    name: Factory reset

  - platform: restart
    id: reboot_btn
    name: restart

sensor:
  #-------------------------------------------------------------------------------
  # ESP32 internal sensors
  #-------------------------------------------------------------------------------
  # Temperature
  - platform: internal_temperature
    name: Internal Temperature
    update_interval: ${device_sampling_time}

  - platform: debug
    free:
      name: Heap free
      id: ${friendly_name}_heap_free
    block:
      name: Heap max block
      id: ${friendly_name}_heap_max_block
    loop_time:
      name: Loop time
      id: ${friendly_name}_loop_time

  - platform: template
    name: Heap free (%)
    id: ${friendly_name}_heap_free_percent
    unit_of_measurement: '%'
    accuracy_decimals: 2
    icon: mdi:percent
    update_interval: ${device_sampling_time}
    entity_category: diagnostic
    # 524288 B (SRAM total)
    # 327680 B (DRAM usable)
    lambda: return ( id(${friendly_name}_heap_free).state * 100 / 327680 );

  - platform: template
    name: Heap max block (%)
    id: ${friendly_name}_heap_max_block_percent
    unit_of_measurement: '%'
    accuracy_decimals: 2
    icon: mdi:percent
    update_interval: ${device_sampling_time}
    entity_category: diagnostic
    # 524288 B (SRAM total)
    # 327680 B (DRAM usable)
    lambda: return ( id(${friendly_name}_heap_max_block).state * 100 / 327680 );

  - platform: uptime
    name: "Uptime Sensor"
    id: ${friendly_name}_uptime_sensor
    update_interval: ${device_sampling_time}
    internal: true
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: ${friendly_name}_uptime_human
            state: !lambda |-
              int seconds = round(id(${friendly_name}_uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")
              ).c_str();

 
text_sensor:
  - platform: ethernet_info
    ip_address:
      name: "IP Address" 

  #-------------------------------------------------------------------------------
  # Custom Text sensors
  #-------------------------------------------------------------------------------
  - platform: template
    name: Uptime Human Readable
    id: ${friendly_name}_uptime_human
    icon: mdi:clock-start    
    

web_server:
  port: 80
  version: 2
  include_internal: true
  ota: false      

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  #ap:
  #  ssid: "Ws-Poe-S3 Fallback Hotspot"
  #  password: "AB6J9ECa7R0F"

#captive_portal:
    

Thanks to the helpful posts above, the camera finally works quite well and with ethernet the BLE proxy works much much better than via WiFi:

No problem with brightness in esphome 2025.05.

Config with Ethernet, Camera and BLE-Proxy:


#===============================================================================
substitutions:
  device_name: you-name-for-your-device
  room_friendly_name: Maybe Your Workshop Room
#===============================================================================

esphome:
  name: ${device_name}
  platformio_options:
    board_build.arduino.memory_type: qio_opi
    board_build.flash_mode: dio
    upload_speed: 921600
    monitor_speed: 115200

esp32:
  board: esp32-s3-devkitc-1
  flash_size: 16MB
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32_S3_BOX_BOARD: "y"

psram:
  mode: octal
  speed: 80MHz

logger:

api:
  encryption:
    key: !secret api_encryption_psk

ota:
  - platform: esphome
    password: !secret ota_password

ethernet:
  type: W5500
  clk_pin: GPIO13
  mosi_pin: GPIO11
  miso_pin: GPIO12
  cs_pin: GPIO14
  interrupt_pin: GPIO10
  reset_pin: GPIO9

######################################
### Camera 
######################################

esp32_camera:
  name: ${room_friendly_name} Camera
  external_clock:
    pin: GPIO3
    frequency: 20MHz
  i2c_pins:
    sda: GPIO48
    scl: GPIO47
  data_pins: [GPIO41, GPIO45, GPIO46, GPIO42, GPIO40, GPIO38, GPIO15, GPIO18]
  vsync_pin: GPIO1
  href_pin: GPIO2
  pixel_clock_pin: GPIO39
  power_down_pin: GPIO8
  resolution: 640x480
  jpeg_quality: 10
    #resolution: 1280x1024
    #jpeg_quality: 30
  vertical_flip: false
  horizontal_mirror: false
  # aec_mode: MANUAL
  # aec_value: 1200
  # brightness: 2
  # wb_mode: HOME
  # agc_mode: AUTO


#######################################
### Bluetooth Proxy
#######################################

esp32_ble_tracker:
  scan_parameters:
    #  only use these values on Ethernet not WIFI
    interval: 1100ms
    window: 1100ms
    active: true

bluetooth_proxy:
  active: true

I also made an enclosure for the board, if anybody needs one:

Thanks to this thread i made it finally work.

now… this thing gets hot? or is it just in my case? (ESP32-S3-ETH, PoE module and also camera)

absolutely … any ESP32 where the camera is in use, gets quite hot in my experience. On top of that, I am sure the POE-module also generates it’s share of heat.

so: yes, that’s normal

Just wanted to thank @bernhardt for this. It’s exactly what I needed to get mine working as a camera and proxy.

I also printed that case and wall mount. They fit perfectly.

Hi everyone. I managed to set up a Waveshare ESP32-S3-ETH W5500 (S3-N16R8) + POE Module (B) + OV5640 (DCXYX-LZTKQJ-5M-357-V1 FF) for web streaming in ESP-IDF 5.3. Ethernet or Wi-Fi is possible. And some nice extras like core temperature, NTP date, and clock. Please insert your own values ​​into " *********** "

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "esp_log.h"
#include "esp_eth.h"
#include "esp_eth_mac_w5500.h"
#include "esp_eth_phy_w5500.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_netif_sntp.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/temperature_sensor.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_camera.h"
#include "esp_http_server.h"
#include "esp_ota_ops.h"
#include "esp_wifi.h"
#include "nvs_flash.h"

static const char *TAG = "********";

#define ETH_CS   14
#define ETH_INT  10
#define ETH_RST  9
#define PWR_CTRL 8 

#define WIFI_SSID "********"
#define WIFI_PASS "*********"

static bool eth_connected = false;

float get_chip_temp() {
    static temperature_sensor_handle_t temp_sensor = NULL;
    if (temp_sensor == NULL) {
        temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 100);
        temperature_sensor_install(&temp_sensor_config, &temp_sensor);
        temperature_sensor_enable(temp_sensor);
    }
    float tsens_out = 0;
    temperature_sensor_get_celsius(temp_sensor, &tsens_out);
    return tsens_out;
}

esp_err_t date_handler(httpd_req_t *req) {
    char buf[16];
    time_t now; struct tm ti;
    time(&now); localtime_r(&now, &ti);
    strftime(buf, sizeof(buf), "%d.%m.%Y", &ti);
    return httpd_resp_send(req, buf, strlen(buf));
}

esp_err_t time_handler(httpd_req_t *req) {
    char buf[16];
    time_t now; struct tm ti;
    time(&now); localtime_r(&now, &ti);
    strftime(buf, sizeof(buf), "%H:%M:%S", &ti);
    return httpd_resp_send(req, buf, strlen(buf));
}

esp_err_t temp_handler(httpd_req_t *req) {
    char buf[16];
    snprintf(buf, sizeof(buf), "%.1f", get_chip_temp());
    return httpd_resp_send(req, buf, strlen(buf));
}

esp_err_t update_post_handler(httpd_req_t *req) {
    esp_ota_handle_t update_handle = 0;
    const esp_partition_t *update_part = esp_ota_get_next_update_partition(NULL);
    if (!update_part) return ESP_FAIL;
    esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &update_handle);
    char *buf = malloc(1024);
    int remaining = req->content_len;
    while (remaining > 0) {
        int recv_len = httpd_req_recv(req, buf, (remaining > 1024) ? 1024 : remaining);
        if (recv_len <= 0) break;
        esp_ota_write(update_handle, buf, recv_len);
        remaining -= recv_len;
    }
    free(buf);
    if (esp_ota_end(update_handle) == ESP_OK && esp_ota_set_boot_partition(update_part) == ESP_OK) {
        ESP_LOGI(TAG, "OTA Success. Rebooting...");
        vTaskDelay(pdMS_TO_TICKS(1000));
        esp_restart();
    }
    return ESP_OK;
}

esp_err_t stream_handler(httpd_req_t *req) {
    camera_fb_t * fb = NULL;
    esp_err_t res = ESP_OK;
    char hdr[64];
    httpd_resp_set_type(req, "multipart/x-mixed-replace;boundary=frame");
    
    while(res == ESP_OK) {
        fb = esp_camera_fb_get();
        if (fb) {
            // ESP_LOGW(TAG, "Frame: %dx%d, Size: %u bytes", fb->width, fb->height, fb->len);
            res = httpd_resp_send_chunk(req, "\r\n--frame\r\n", 11);
            if(res == ESP_OK) {
                int hlen = snprintf(hdr, 64, "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", fb->len);
                res = httpd_resp_send_chunk(req, hdr, hlen);
            }
            if(res == ESP_OK) res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
            esp_camera_fb_return(fb);
        } else {
            ESP_LOGE(TAG, "Camera capture failed");
            res = ESP_FAIL;
        }
        vTaskDelay(pdMS_TO_TICKS(250)); // 4 FPS 
    }
    return res;
}

esp_err_t index_handler(httpd_req_t *req) {
    const char* html = "<html><head><meta charset='utf-8'><style>"
        "body{margin:0;background:#000;color:#fff;font-family:sans-serif;overflow:hidden;padding:0;}"
        ".container{display:flex; justify-content:center; align-items:flex-start; width:100vw; height:100vh;}"
        ".video-wrapper{position:relative; display:inline-block; max-width:100%; max-height:100%;}"
        "img{display:block; max-width:100%; max-height:100%; object-fit:contain;}" 
        ".osd{position:absolute; top:10px; right:10px; font-size:14px; font-weight:bold; text-align:right;"
        "color:#fff; text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000; z-index:10; pointer-events:none;}"
        "</style></head><body>"
        "<div class='container'>"
          "<div class='video-wrapper'>"
            "<img id='stream_img' src=''>"
            "<div class='osd'>CAM 02<br><span id='date'>--.--.----</span><br>"
            "<span id='time'>--:--:--</span><br><span id='temp'>--</span>°C</div>"
          "</div>"
        "</div>"
        "<script>document.getElementById('stream_img').src='http://'+window.location.hostname+':81/stream';"
        "async function update(){try{"
        "const[t,c,d]=await Promise.all([fetch('/temp'),fetch('/time'),fetch('/date')]);"
        "if(t.ok)document.getElementById('temp').innerText=await t.text();"
        "if(c.ok)document.getElementById('time').innerText=await c.text();"
        "if(d.ok)document.getElementById('date').innerText=await d.text();"
        "}catch(e){}setTimeout(update,2000);}update();</script></body></html>";
    return httpd_resp_send(req, html, strlen(html));
}

static camera_config_t camera_config = {
    .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 3,
    .pin_sccb_sda = 48, .pin_sccb_scl = 47,
    .pin_d7 = 18, .pin_d6 = 15, .pin_d5 = 38, .pin_d4 = 40,
    .pin_d3 = 42, .pin_d2 = 46, .pin_d1 = 45, .pin_d0 = 41,
    .pin_vsync = 1, .pin_href = 2, .pin_pclk = 39,
    .xclk_freq_hz = 20000000, .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0,
    .pixel_format = PIXFORMAT_JPEG, 
    .frame_size = FRAMESIZE_XGA,    
    .jpeg_quality = 3,            
    .fb_count = 2, .fb_location = CAMERA_FB_IN_PSRAM,
    .grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};

// --- [CORE 1 TASK] ---
void camera_server_task(void *pvParameters) {
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
        vTaskDelete(NULL);
        return;
    }

    sensor_t * s = esp_camera_sensor_get();
    if (s) {
        s->set_vflip(s, 1);
        s->set_hmirror(s, 1);
        s->set_reg(s, 0x3035, 0xff, 0xA1); 
        s->set_whitebal(s, 1);   
        s->set_gain_ctrl(s, 1);  
        s->set_aec_value(s, 1);
        s->set_brightness(s, 0);   
        s->set_contrast(s, -1);     
        s->set_saturation(s, -1);   
        s->set_sharpness(s, 2);    
        s->set_ae_level(s, 0);     
    }

    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_uri_handlers = 12;
    
    // PORT 80
    config.server_port = 80;
    httpd_handle_t server_data = NULL;
    if (httpd_start(&server_data, &config) == ESP_OK) {
        httpd_uri_t uri_idx = { .uri = "/", .method = HTTP_GET, .handler = index_handler };
        httpd_uri_t uri_dt  = { .uri = "/date", .method = HTTP_GET, .handler = date_handler };
        httpd_uri_t uri_ti  = { .uri = "/time", .method = HTTP_GET, .handler = time_handler };
        httpd_uri_t uri_tp  = { .uri = "/temp", .method = HTTP_GET, .handler = temp_handler };
        httpd_uri_t uri_ota = { .uri = "/update", .method = HTTP_POST, .handler = update_post_handler };
        httpd_register_uri_handler(server_data, &uri_idx);
        httpd_register_uri_handler(server_data, &uri_dt);
        httpd_register_uri_handler(server_data, &uri_ti);
        httpd_register_uri_handler(server_data, &uri_tp);
        httpd_register_uri_handler(server_data, &uri_ota);
       
    }

    // PORT 81
    config.server_port = 81;
    config.ctrl_port = 32769;
    httpd_handle_t server_stream = NULL;
    if (httpd_start(&server_stream, &config) == ESP_OK) {
        httpd_uri_t uri_str = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler };
        httpd_register_uri_handler(server_stream, &uri_str);
    }
    
    ESP_LOGI(TAG, "Camera and Servers started on Core 1");
    while (1) vTaskDelay(pdMS_TO_TICKS(1000));
}

static void event_handler(void* arg, esp_event_base_t base, int32_t id, void* data) {
    if (base == ETH_EVENT) {
        if (id == ETHERNET_EVENT_CONNECTED) {
            ESP_LOGW(TAG, "Ethernet Connected. Disabling Wi-Fi...");
            eth_connected = true;
            esp_wifi_stop(); 
        } else if (id == ETHERNET_EVENT_DISCONNECTED) {
            ESP_LOGW(TAG, "Ethernet Disconnected. Enabling Wi-Fi...");
            eth_connected = false;
            esp_wifi_start();
        }
    } else if (base == WIFI_EVENT) {
        if (id == WIFI_EVENT_STA_START || id == WIFI_EVENT_STA_DISCONNECTED) {
            if (!eth_connected) { 
                ESP_LOGI(TAG, "Wi-Fi connecting...");
                esp_wifi_connect();
            }
        }
    } else if (base == IP_EVENT) {
        if (id == IP_EVENT_STA_GOT_IP || id == IP_EVENT_ETH_GOT_IP) {
            ESP_LOGI(TAG, "IP Received!");
            esp_sntp_config_t sntp_cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG("**Your Gate**");
            esp_netif_sntp_init(&sntp_cfg);
        }
    }
}

void app_main(void) {
    nvs_flash_init();
    gpio_install_isr_service(0);
    
    gpio_set_direction(PWR_CTRL, GPIO_MODE_OUTPUT); 
    gpio_set_level(PWR_CTRL, 0); 
    vTaskDelay(pdMS_TO_TICKS(500));
    
    gpio_set_direction(ETH_RST, GPIO_MODE_OUTPUT); 
    gpio_set_level(ETH_RST, 0);
    vTaskDelay(pdMS_TO_TICKS(100)); 
    gpio_set_level(ETH_RST, 1);
        
    vTaskDelay(pdMS_TO_TICKS(500)); 
       
    esp_netif_init();
    esp_event_loop_create_default();

    esp_netif_config_t eth_cfg = ESP_NETIF_DEFAULT_ETH();
    esp_netif_t *eth_netif = esp_netif_new(&eth_cfg);
    spi_bus_config_t buscfg = { .miso_io_num = 12, .mosi_io_num = 11, .sclk_io_num = 13, .quadwp_io_num = -1, .quadhd_io_num = -1 };
    spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
    spi_device_interface_config_t devcfg = { .command_bits = 16, .address_bits = 8, .mode = 0, .clock_speed_hz = 25000000, .spics_io_num = ETH_CS, .queue_size = 30 };
    eth_w5500_config_t w5500_cfg = ETH_W5500_DEFAULT_CONFIG(SPI2_HOST, &devcfg);
    w5500_cfg.int_gpio_num = ETH_INT;
    esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_cfg, &(eth_mac_config_t)ETH_MAC_DEFAULT_CONFIG());
    esp_eth_phy_t *phy = esp_eth_phy_new_w5500(&(eth_phy_config_t)ETH_PHY_DEFAULT_CONFIG());
    esp_eth_handle_t eth_handle = NULL;
    esp_eth_driver_install(&(esp_eth_config_t)ETH_DEFAULT_CONFIG(mac, phy), &eth_handle);
    uint8_t eth_mac_addr[6] = {0xE8, 0xF6, 0x0A, 0x84, 0x9D, 0xD5};
    mac->set_addr(mac, eth_mac_addr);
    esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle));

    esp_netif_create_default_wifi_sta();
    wifi_init_config_t w_cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&w_cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    uint8_t wifi_mac_addr[6] = {0xE8, 0xF6, 0x0A, 0x84, 0x9D, 0xD4};
    esp_wifi_set_mac(WIFI_IF_STA, wifi_mac_addr);
    wifi_config_t wifi_config = { .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS } };
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);

    esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL);
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL);
    esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL);

    esp_eth_start(eth_handle);
    esp_wifi_start();

    setenv("TZ", "*************", 1);
    tzset();

    vTaskDelay(pdMS_TO_TICKS(2000));
    xTaskCreatePinnedToCore(camera_server_task, "cam_task", 8192, NULL, 5, NULL, 1);

    while (1) vTaskDelay(pdMS_TO_TICKS(1000));
}

After moving I needed to rebuild my HA setup for the new place. Unfortently after reinstalling ESP Home and the script from @bernhardt the camera no longer works. All I get is this: [W][esp32_camera:211]: Got invalid frame from camera!

Does anyone know what I am doing wrong?

first of all, THANK YOU! just got my board, and ethernet is already working (camera is on the way, prob it is gonna work too).

do you guys know any available GPIO i could hook a normally open push button? i’d like to use as a doorbel


EDIT1: ok, GPIO16 looks good