Skip to content

Instantly share code, notes, and snippets.

@hectorzin
Last active June 19, 2026 08:52
Show Gist options
  • Select an option

  • Save hectorzin/25483aac4dc888cddab0302b8d19a039 to your computer and use it in GitHub Desktop.

Select an option

Save hectorzin/25483aac4dc888cddab0302b8d19a039 to your computer and use it in GitHub Desktop.
CrowPanel Advanced 10 1inch ESP32 P4 HMI AI Display ESPHome Examples
name: "CrowPanel Home Assistant WebView"
version: "0.1.5"
slug: "remote_webview_server"
description: "Streams your HA dashboard or any website to ESP32P4 CrowPanel via WebSocket."
url: "https://github.com/fintros/RemoteWebViewServer/tree/main/hassio/remote_webview_server"
arch:
- amd64
- aarch64
startup: application
boot: auto
host_network: true
video: true
udev: true
init: false
watchdog: "http://[HOST]:[PORT:18080]/"
map:
- type: addon_config
read_only: false
options:
tile_size: 256
full_frame_tile_count: 4
full_frame_area_threshold: 0.5
full_frame_every: 50
every_nth_frame: 1
min_frame_interval_ms: 80
jpeg_quality: 85
max_bytes_per_message: 122880
ws_port: 8081
debug_port: 9221
expose_debug_proxy: false
debug_proxy_port: 9222
health_port: 18080
prefers_reduced_motion: false
inject_js_url: ""
inject_js_allow_http: false
user_data_dir: "/pw-data"
schema:
tile_size: int
full_frame_tile_count: int
full_frame_area_threshold: float
full_frame_every: int
every_nth_frame: int
min_frame_interval_ms: int
jpeg_quality: int
max_bytes_per_message: int
ws_port: int
debug_port: int
expose_debug_proxy: bool
debug_proxy_port: int
health_port: int
prefers_reduced_motion: bool
inject_js_url: str
inject_js_allow_http: bool
user_data_dir: str
FROM mcr.microsoft.com/playwright:v1.60.0-jammy
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
jq \
socat \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/fintros/RemoteWebViewServer.git /src
WORKDIR /src
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN npm ci --no-audit --no-fund
RUN npm run build
RUN npm prune --omit=dev
RUN cp -r dist /app/dist
RUN cp -r node_modules /app/node_modules
WORKDIR /app
COPY run.sh /run.sh
RUN chmod +x /run.sh
EXPOSE 8081 18080 9221
CMD ["/run.sh"]
#!/usr/bin/env bash
set -euo pipefail
OPTIONS_FILE="/data/options.json"
get_opt() {
local key="$1" default="$2"
if [ -f "$OPTIONS_FILE" ]; then
jq -r --arg k "$key" --arg d "$default" '.[$k] // $d' "$OPTIONS_FILE"
else
echo "$default"
fi
}
export TILE_SIZE="$(get_opt tile_size 32)"
export FULL_FRAME_TILE_COUNT="$(get_opt full_frame_tile_count 4)"
export FULL_FRAME_AREA_THRESHOLD="$(get_opt full_frame_area_threshold 0.5)"
export FULL_FRAME_EVERY="$(get_opt full_frame_every 50)"
export EVERY_NTH_FRAME="$(get_opt every_nth_frame 1)"
export MIN_FRAME_INTERVAL_MS="$(get_opt min_frame_interval_ms 80)"
export JPEG_QUALITY="$(get_opt jpeg_quality 85)"
export MAX_BYTES_PER_MESSAGE="$(get_opt max_bytes_per_message 14336)"
export WS_PORT="$(get_opt ws_port 8081)"
export DEBUG_PORT="$(get_opt debug_port 9221)"
export HEALTH_PORT="$(get_opt health_port 18080)"
export PREFERS_REDUCED_MOTION="$(get_opt prefers_reduced_motion false)"
export INJECT_JS_URL="$(get_opt inject_js_url "")"
export INJECT_JS_ALLOW_HTTP="$(get_opt inject_js_allow_http false)"
USER_DATA_DIR_OPT="$(get_opt user_data_dir "/pw-data")"
if [ "$USER_DATA_DIR_OPT" = "/pw-data" ]; then
mkdir -p /data/pw-data
# if not already a symlink, make /pw-data -> /data/pw-data
if [ ! -L /pw-data ]; then
rm -rf /pw-data 2>/dev/null || true
ln -s /data/pw-data /pw-data
fi
export USER_DATA_DIR="/pw-data"
else
mkdir -p "$USER_DATA_DIR_OPT"
export USER_DATA_DIR="$USER_DATA_DIR_OPT"
fi
EXPOSE_DEBUG_PROXY="$(get_opt expose_debug_proxy false)"
if [ "$EXPOSE_DEBUG_PROXY" = "true" ]; then
DEBUG_PROXY_PORT="$(get_opt debug_proxy_port 9222)"
echo "[remote-webview] Starting debug proxy on :${DEBUG_PROXY_PORT} -> 127.0.0.1:${DEBUG_PORT}"
socat -d -d "TCP-LISTEN:${DEBUG_PROXY_PORT},fork,reuseaddr,keepalive" "TCP:127.0.0.1:${DEBUG_PORT}" &
fi
command -v node >/dev/null
test -f dist/index.js
exec node dist/index.js
esphome:
name: advance-p4-10-1-inch
friendly_name: Advance-P4-10.1-inch
on_boot:
priority: 800
then:
- logger.log: "Backlight ON"
- output.turn_on: backlight_pwm
- delay: 200ms
- output.turn_off: power_light
- delay: 500ms
- light.turn_on:
id: back_light
brightness: 1
esp32:
variant: esp32p4
engineering_sample: true
cpu_frequency: 360MHZ
flash_size: 16MB
framework:
type: esp-idf
advanced:
execute_from_psram: true
enable_idf_experimental_features: true
psram:
speed: 200MHz
esp_ldo:
- channel: 3
voltage: 2.5V
- channel: 4
voltage: 2.5V
# Enable logging
logger:
level: debug
logs:
lvgl: info
hardware_uart: UART0
# Enable Home Assistant API
api:
encryption:
key: "Dp4Wgl1jj5F++6mnzSma7lHvgHCGfghlE/ohT96S+jw="
ota:
- platform: esphome
password: "bf1c7324cdcd0f41044c7722cbf5a048"
esp32_hosted:
variant: esp32c6
active_high: true
reset_pin: GPIO32
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO17
d1_pin: GPIO16
d2_pin: GPIO15
d3_pin: GPIO14
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Advance-P4-10.1-Inch"
password: "eW0iGlqyJxnC"
captive_portal:
i2c:
- id: bus_a
sda: GPIO45
scl: GPIO46
frequency: 400kHz
sensor:
- platform: aht10
variant: AHT20
temperature:
name: "Room Temperature"
id: room_temp
unit_of_measurement: "°C"
accuracy_decimals: 1
humidity:
name: "Room Humidity"
id: room_hum
unit_of_measurement: "%"
accuracy_decimals: 1
update_interval: 1s
touchscreen:
- platform: gt911
id: my_touchscreen
i2c_id: bus_a
reset_pin: 40
interrupt_pin: 42
update_interval: 50ms
transform:
swap_xy: false
mirror_x: false
mirror_y: false
# Enabled Touch Logger here
on_touch:
- logger.log:
format: "Touch at (%d, %d)"
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
switch:
- platform: gpio
pin: GPIO48
name: "LED"
id: led_output
on_turn_on:
# Switch bulb diagram
- lvgl.image.update:
id: lvgl_light
src: light_on
# Switch the status label text to ON
- lvgl.label.update:
id: label_light_state
text: "ON"
on_turn_off:
# Switch bulb diagram
- lvgl.image.update:
id: lvgl_light
src: light_off
# Switch the status label text to OFF
- lvgl.label.update:
id: label_light_state
text: "OFF"
output:
- platform: ledc
pin: GPIO31
id: backlight_pwm
- platform: gpio
id: power_light
pin: GPIO29
inverted: true
light:
- platform: monochromatic
name: "Backlight"
output: backlight_pwm
id: back_light
restore_mode: ALWAYS_ON
internal: true
display:
- platform: mipi_dsi
id: my_display
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B
reset_pin:
number: 41
update_interval: never
auto_clear_enabled: false
dimensions:
width: 1024
height: 600
color_order: RGB
color_depth: 16
image:
- file: "small_temp.png"
id: small_temp
resize: 200x200
type: RGB565
byte_order: little_endian
- file: "small_hum.png"
id: small_hum
resize: 200x200
type: RGB565
byte_order: little_endian
- file: "small_logo.png"
id: small_logo
resize: 200x100
type: RGB565
byte_order: little_endian
- file: "light.png"
id: light_on
resize: 200x200
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "no_light.png"
id: light_off
resize: 200x200
type: RGB565
transparency: alpha_channel
byte_order: little_endian
lvgl:
buffer_size: 50%
byte_order: little_endian
color_depth: 16
log_level: INFO
bg_color: 0xFFFFFF
text_font: montserrat_26
widgets:
- image:
id: lvgl_logo
src: small_logo
x: 400
y: 20
- image:
id: lvgl_temp
src: small_temp
x: 250
y: 250
- image:
id: lvgl_hum
src: small_hum
x: 500
y: 250
- image:
id: lvgl_light
src: light_off # Default 'Turn off lights' image
x: 750 # You can adjust the position by yourself
y: 250
- label:
id: label_temp
x: 190
y: 350
text: "Temp: --°C"
- label:
id: label_hum
x: 450
y: 350
text: "Humi: --%"
- label:
id: label_light_state
x: 750
y: 350
text: "OFF"
interval:
- interval: 1s
then:
- lambda: |-
char temp_str[20];
char hum_str[20];
snprintf(temp_str, sizeof(temp_str), "Temp: %.1f°C", id(room_temp).state);
snprintf(hum_str, sizeof(hum_str), "Humi: %.1f%%", id(room_hum).state);
lv_label_set_text(id(label_temp), temp_str);
lv_label_set_text(id(label_hum), hum_str);
esphome:
name: advance-p4-10-1-inch
friendly_name: Advance-P4-10.1-inch
on_boot:
priority: 800
then:
- logger.log: "Backlight ON"
- output.turn_on: backlight_pwm
- delay: 200ms
- output.turn_off: power_light
- delay: 500ms
- light.turn_on:
id: back_light
brightness: 1
esp32:
variant: esp32p4
engineering_sample: true
cpu_frequency: 360MHZ
flash_size: 16MB
framework:
type: esp-idf
advanced:
execute_from_psram: true
enable_idf_experimental_features: true
psram:
speed: 200MHz
esp_ldo:
- channel: 3
voltage: 2.5V
- channel: 4
voltage: 2.5V
# Enable logging
logger:
level: debug
logs:
lvgl: info
hardware_uart: UART0
# Enable Home Assistant API
api:
encryption:
key: "Dp4Wgl1jj5F++6mnzSma7lHvgHCGfghlE/ohT96S+jw="
ota:
- platform: esphome
password: "bf1c7324cdcd0f41044c7722cbf5a048"
esp32_hosted:
variant: esp32c6
active_high: true
reset_pin: GPIO32
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO17
d1_pin: GPIO16
d2_pin: GPIO15
d3_pin: GPIO14
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Advance-P4-10.1-Inch"
password: "eW0iGlqyJxnC"
captive_portal:
i2c:
- id: bus_a
sda: GPIO45
scl: GPIO46
frequency: 400kHz
sensor:
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_temperature
id: room_temp
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_humidity
id: room_hum
touchscreen:
- platform: gt911
id: my_touchscreen
i2c_id: bus_a
reset_pin: 40
interrupt_pin: 42
update_interval: 50ms
transform:
swap_xy: false
mirror_x: false
mirror_y: false
# Enabled Touch Logger here
on_touch:
- logger.log:
format: "Touch at (%d, %d)"
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
switch:
- platform: gpio
pin: GPIO48
name: "LED"
id: led_output
on_turn_on:
# Switch bulb diagram
- lvgl.image.update:
id: lvgl_light
src: light_on
# Switch the status label text to ON
- lvgl.label.update:
id: label_light_state
text: "ON"
on_turn_off:
# Switch bulb diagram
- lvgl.image.update:
id: lvgl_light
src: light_off
# Switch the status label text to OFF
- lvgl.label.update:
id: label_light_state
text: "OFF"
output:
- platform: ledc
pin: GPIO31
id: backlight_pwm
- platform: gpio
id: power_light
pin: GPIO29
inverted: true
light:
- platform: monochromatic
name: "Backlight"
output: backlight_pwm
id: back_light
restore_mode: ALWAYS_ON
internal: true
display:
- platform: mipi_dsi
id: my_display
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B
reset_pin:
number: 41
update_interval: never
auto_clear_enabled: false
dimensions:
width: 1024
height: 600
color_order: RGB
color_depth: 16
image:
- file: "small_temp.png"
id: small_temp
resize: 200x200
type: RGB565
byte_order: little_endian
- file: "small_hum.png"
id: small_hum
resize: 200x200
type: RGB565
byte_order: little_endian
- file: "small_logo.png"
id: small_logo
resize: 200x100
type: RGB565
byte_order: little_endian
- file: "light.png"
id: light_on
resize: 200x200
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "no_light.png"
id: light_off
resize: 200x200
type: RGB565
transparency: alpha_channel
byte_order: little_endian
lvgl:
buffer_size: 50%
byte_order: little_endian
color_depth: 16
log_level: INFO
bg_color: 0xFFFFFF
text_font: montserrat_26
widgets:
- image:
id: lvgl_logo
src: small_logo
x: 400
y: 20
- image:
id: lvgl_temp
src: small_temp
x: 250
y: 250
- image:
id: lvgl_hum
src: small_hum
x: 500
y: 250
- image:
id: lvgl_light
src: light_off # Default 'Turn off lights' image
x: 750 # You can adjust the position by yourself
y: 250
- label:
id: label_temp
x: 190
y: 350
text: "Temp: --°C"
- label:
id: label_hum
x: 450
y: 350
text: "Humi: --%"
- label:
id: label_light_state
x: 750
y: 350
text: "OFF"
interval:
- interval: 1s
then:
- lambda: |-
char temp_str[20];
char hum_str[20];
snprintf(temp_str, sizeof(temp_str), "Temp: %.1f°C", id(room_temp).state);
snprintf(hum_str, sizeof(hum_str), "Humi: %.1f%%", id(room_hum).state);
lv_label_set_text(id(label_temp), temp_str);
lv_label_set_text(id(label_hum), hum_str);
esphome:
name: advance-p4-10-1-inch
friendly_name: Advance-P4-10.1-inch
on_boot:
priority: 800
then:
- logger.log: "Backlight ON"
- output.turn_on: backlight_pwm
- delay: 200ms
- output.turn_off: power_light
- delay: 500ms
- light.turn_on:
id: back_light
brightness: 1
esp32:
variant: esp32p4
engineering_sample: true
cpu_frequency: 360MHZ
flash_size: 16MB
framework:
type: esp-idf
advanced:
execute_from_psram: true
enable_idf_experimental_features: true
psram:
speed: 200MHz
esp_ldo:
- channel: 3
voltage: 2.5V
- channel: 4
voltage: 2.5V
# Enable logging
logger:
level: debug
logs:
lvgl: info
hardware_uart: UART0
# Enable Home Assistant API
api:
encryption:
key: "Dp4Wgl1jj5F++6mnzSma7lHvgHCGfghlE/ohT96S+jw="
ota:
- platform: esphome
password: "bf1c7324cdcd0f41044c7722cbf5a048"
esp32_hosted:
variant: esp32c6
active_high: true
reset_pin: GPIO32
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO17
d1_pin: GPIO16
d2_pin: GPIO15
d3_pin: GPIO14
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Advance-P4-10.1-Inch"
password: "eW0iGlqyJxnC"
captive_portal:
i2c:
- id: bus_a
sda: GPIO45
scl: GPIO46
frequency: 400kHz
sensor:
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_temperature
id: room_temp
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_humidity
id: room_hum
- platform: homeassistant
entity_id: sensor.i_9psl_pm2_5
id: pm25
- platform: homeassistant
entity_id: sensor.i_9psl_pm10
id: pm10
- platform: homeassistant
entity_id: sensor.i_9psl_pm1
id: pm1
- platform: homeassistant
entity_id: sensor.i_9psl_dioxido_de_carbono
id: co2
- platform: homeassistant
entity_id: sensor.i_9psl_indice_cov
id: voc
- platform: homeassistant
entity_id: sensor.i_9psl_indice_nox
id: nox
touchscreen:
- platform: gt911
id: my_touchscreen
i2c_id: bus_a
reset_pin: 40
interrupt_pin: 42
update_interval: 50ms
transform:
swap_xy: false
mirror_x: false
mirror_y: false
# Enabled Touch Logger here
on_touch:
- logger.log:
format: "Touch at (%d, %d)"
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
switch:
- platform: gpio
pin: GPIO48
name: "LED"
id: led_output
output:
- platform: ledc
pin: GPIO31
id: backlight_pwm
- platform: gpio
id: power_light
pin: GPIO29
inverted: true
light:
- platform: monochromatic
name: "Backlight"
output: backlight_pwm
id: back_light
restore_mode: ALWAYS_ON
internal: true
display:
- platform: mipi_dsi
id: my_display
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B
reset_pin:
number: 41
update_interval: never
auto_clear_enabled: false
dimensions:
width: 1024
height: 600
color_order: RGB
color_depth: 16
image:
- file: "temperatura_256.png"
id: icon_temp
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "humedad_256.png"
id: icon_hum
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "co2_256.png"
id: icon_co2
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "pm25_256.png"
id: icon_pm25
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "pm10_256.png"
id: icon_pm10
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "voc_256.png"
id: icon_voc
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "nox_256.png"
id: icon_nox
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
lvgl:
buffer_size: 50%
byte_order: little_endian
color_depth: 16
log_level: INFO
bg_color: 0xFFFFFF
text_font: montserrat_26
widgets:
- label:
id: titulo
text: "CALIDAD DEL AIRE INTERIOR"
x: 290
y: 20
- label:
id: estado_aire
text: "BUENO"
x: 450
y: 65
# FILA 1
- image:
src: icon_temp
x: 80
y: 130
- label:
id: temperatura
text: "--.- C"
x: 40
y: 240
- image:
src: icon_hum
x: 320
y: 130
- label:
id: humedad
text: "-- %"
x: 300
y: 240
- image:
src: icon_co2
x: 560
y: 130
- label:
id: co2_label
text: "--- ppm"
x: 500
y: 240
- image:
src: icon_voc
x: 800
y: 130
- label:
id: voc_label
text: "---"
x: 820
y: 240
# FILA 2
- image:
src: icon_nox
x: 80
y: 340
- label:
id: nox_label
text: "---"
x: 110
y: 450
- image:
src: icon_pm25
x: 320
y: 340
- label:
id: pm1_label
text: "PM1 --.-"
x: 270
y: 450
- image:
src: icon_pm25
x: 560
y: 340
- label:
id: pm25_label
text: "PM2.5 --.-"
x: 500
y: 450
- image:
src: icon_pm10
x: 800
y: 340
- label:
id: pm10_label
text: "PM10 --.-"
x: 740
y: 450
interval:
- interval: 1s
then:
- lambda: |-
char txt[64];
snprintf(txt, sizeof(txt), "Temp: %.1f °C", id(room_temp).state);
lv_label_set_text(id(temperatura), txt);
snprintf(txt, sizeof(txt), "Hum: %.0f %%", id(room_hum).state);
lv_label_set_text(id(humedad), txt);
snprintf(txt, sizeof(txt), "CO2: %.0f ppm", id(co2).state);
lv_label_set_text(id(co2_label), txt);
snprintf(txt, sizeof(txt), "VOC: %.0f", id(voc).state);
lv_label_set_text(id(voc_label), txt);
snprintf(txt, sizeof(txt), "NOx: %.0f", id(nox).state);
lv_label_set_text(id(nox_label), txt);
snprintf(txt, sizeof(txt), "PM1: %.1f", id(pm1).state);
lv_label_set_text(id(pm1_label), txt);
snprintf(txt, sizeof(txt), "PM2.5: %.1f", id(pm25).state);
lv_label_set_text(id(pm25_label), txt);
snprintf(txt, sizeof(txt), "PM10: %.1f", id(pm10).state);
lv_label_set_text(id(pm10_label), txt);
int sev_co2 =
id(co2).state <= 800 ? 0 :
id(co2).state <= 1000 ? 1 :
id(co2).state <= 1400 ? 2 :
id(co2).state <= 2000 ? 3 : 4;
int sev_pm25 =
id(pm25).state <= 10 ? 0 :
id(pm25).state <= 15 ? 1 :
id(pm25).state <= 25 ? 2 :
id(pm25).state <= 35 ? 3 :
id(pm25).state <= 55 ? 4 : 5;
int sev_pm10 =
id(pm10).state <= 20 ? 0 :
id(pm10).state <= 45 ? 2 :
id(pm10).state <= 90 ? 3 : 4;
int sev_voc =
id(voc).state <= 100 ? 0 :
id(voc).state <= 200 ? 2 :
id(voc).state <= 300 ? 3 : 4;
int sev_nox =
id(nox).state <= 50 ? 0 :
id(nox).state <= 100 ? 2 :
id(nox).state <= 200 ? 3 : 4;
int worst = sev_co2;
if (sev_pm25 > worst) worst = sev_pm25;
if (sev_pm10 > worst) worst = sev_pm10;
if (sev_voc > worst) worst = sev_voc;
if (sev_nox > worst) worst = sev_nox;
const char *estado[] = {
"BUENO",
"ACEPTABLE",
"MODERADO",
"REGULAR",
"MALO",
"MUY MALO"
};
lv_label_set_text(id(estado_aire), estado[worst]);
esphome:
name: advance-p4-10-1-inch
friendly_name: Advance-P4-10.1-inch
on_boot:
priority: 800
then:
- logger.log: "Backlight ON"
- output.turn_on: backlight_pwm
- delay: 200ms
- output.turn_off: power_light
- delay: 500ms
- light.turn_on:
id: back_light
brightness: 1
esp32:
variant: esp32p4
engineering_sample: true
cpu_frequency: 360MHZ
flash_size: 16MB
framework:
type: esp-idf
advanced:
execute_from_psram: true
enable_idf_experimental_features: true
psram:
speed: 200MHz
esp_ldo:
- channel: 3
voltage: 2.5V
- channel: 4
voltage: 2.5V
# Enable logging
logger:
level: debug
logs:
lvgl: info
hardware_uart: UART0
# Enable Home Assistant API
api:
encryption:
key: "Dp4Wgl1jj5F++6mnzSma7lHvgHCGfghlE/ohT96S+jw="
ota:
- platform: esphome
password: "bf1c7324cdcd0f41044c7722cbf5a048"
esp32_hosted:
variant: esp32c6
active_high: true
reset_pin: GPIO32
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO17
d1_pin: GPIO16
d2_pin: GPIO15
d3_pin: GPIO14
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Advance-P4-10.1-Inch"
password: "eW0iGlqyJxnC"
captive_portal:
i2c:
- id: bus_a
sda: GPIO45
scl: GPIO46
frequency: 400kHz
sensor:
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_temperature
id: room_temp
- platform: homeassistant
entity_id: sensor.ws2900_v2_02_03_indoor_humidity
id: room_hum
- platform: homeassistant
entity_id: sensor.i_9psl_pm2_5
id: pm25
- platform: homeassistant
entity_id: sensor.i_9psl_pm10
id: pm10
- platform: homeassistant
entity_id: sensor.i_9psl_pm1
id: pm1
- platform: homeassistant
entity_id: sensor.i_9psl_dioxido_de_carbono
id: co2
- platform: homeassistant
entity_id: sensor.i_9psl_indice_cov
id: voc
- platform: homeassistant
entity_id: sensor.i_9psl_indice_nox
id: nox
touchscreen:
- platform: gt911
id: my_touchscreen
i2c_id: bus_a
reset_pin: 40
interrupt_pin: 42
update_interval: 50ms
transform:
swap_xy: false
mirror_x: false
mirror_y: false
# Enabled Touch Logger here
on_touch:
- logger.log:
format: "Touch at (%d, %d)"
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
switch:
- platform: gpio
pin: GPIO48
name: "LED"
id: led_output
output:
- platform: ledc
pin: GPIO31
id: backlight_pwm
- platform: gpio
id: power_light
pin: GPIO29
inverted: true
light:
- platform: monochromatic
name: "Backlight"
output: backlight_pwm
id: back_light
restore_mode: ALWAYS_ON
internal: true
display:
- platform: mipi_dsi
id: my_display
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B
reset_pin:
number: 41
update_interval: never
auto_clear_enabled: false
dimensions:
width: 1024
height: 600
color_order: RGB
color_depth: 16
image:
- file: "temperatura_256.png"
id: icon_temp
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "humedad_256.png"
id: icon_hum
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "co2_256.png"
id: icon_co2
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "pm25_256.png"
id: icon_pm25
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "pm10_256.png"
id: icon_pm10
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "pm1_256.png"
id: icon_pm1
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "voc_256.png"
id: icon_voc
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
- file: "nox_256.png"
id: icon_nox
resize: 96x96
type: RGB565
transparency: alpha_channel
byte_order: little_endian
lvgl:
buffer_size: 50%
byte_order: little_endian
color_depth: 16
log_level: INFO
bg_color: 0xFFFFFF
text_font: montserrat_26
pages:
- id: page_sensores
widgets:
- label:
id: titulo
text: "CALIDAD DEL AIRE INTERIOR"
x: 290
y: 20
- label:
id: estado_aire
text: "BUENO"
x: 450
y: 65
- image:
src: icon_temp
x: 80
y: 130
- label:
id: temperatura
text: "--.- C"
x: 40
y: 240
- image:
src: icon_hum
x: 320
y: 130
- label:
id: humedad
text: "-- %"
x: 300
y: 240
- image:
src: icon_co2
x: 560
y: 130
- label:
id: co2_label
text: "--- ppm"
x: 500
y: 240
- image:
src: icon_voc
x: 800
y: 130
- label:
id: voc_label
text: "---"
x: 820
y: 240
- image:
src: icon_nox
x: 80
y: 340
- label:
id: nox_label
text: "---"
x: 110
y: 450
- image:
src: icon_pm1
x: 320
y: 340
- label:
id: pm1_label
text: "PM1 --.-"
x: 270
y: 450
- image:
src: icon_pm25
x: 560
y: 340
- label:
id: pm25_label
text: "PM2.5 --.-"
x: 500
y: 450
- image:
src: icon_pm10
x: 800
y: 340
- label:
id: pm10_label
text: "PM10 --.-"
x: 740
y: 450
- id: page_notificacion
bg_color: 0x000000
widgets:
- label:
id: notif_text
text: "NOTIFICACION"
x: 180
y: 210
text_font: montserrat_48
text_color: 0xFF0000
text:
- platform: template
name: "Mensaje CrowPanel"
id: mensaje_crowpanel
optimistic: true
mode: text
on_value:
then:
- lvgl.page.show: page_notificacion
- lambda: |-
lv_label_set_text(id(notif_text), x.c_str());
lv_obj_set_x(id(notif_text), 1024);
static lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, id(notif_text));
lv_anim_set_values(&anim, 1024, -1400);
lv_anim_set_time(&anim, 12000);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t) lv_obj_set_x);
lv_anim_start(&anim);
- delay: 15s
- lvgl.page.show: page_sensores
interval:
- interval: 1s
then:
- lambda: |-
char txt[64];
snprintf(txt, sizeof(txt), "Temp: %.1f °C", id(room_temp).state);
lv_label_set_text(id(temperatura), txt);
snprintf(txt, sizeof(txt), "Hum: %.0f %%", id(room_hum).state);
lv_label_set_text(id(humedad), txt);
snprintf(txt, sizeof(txt), "CO2: %.0f ppm", id(co2).state);
lv_label_set_text(id(co2_label), txt);
snprintf(txt, sizeof(txt), "VOC: %.0f", id(voc).state);
lv_label_set_text(id(voc_label), txt);
snprintf(txt, sizeof(txt), "NOx: %.0f", id(nox).state);
lv_label_set_text(id(nox_label), txt);
snprintf(txt, sizeof(txt), "PM1: %.1f", id(pm1).state);
lv_label_set_text(id(pm1_label), txt);
snprintf(txt, sizeof(txt), "PM2.5: %.1f", id(pm25).state);
lv_label_set_text(id(pm25_label), txt);
snprintf(txt, sizeof(txt), "PM10: %.1f", id(pm10).state);
lv_label_set_text(id(pm10_label), txt);
int sev_co2 =
id(co2).state <= 800 ? 0 :
id(co2).state <= 1000 ? 1 :
id(co2).state <= 1400 ? 2 :
id(co2).state <= 2000 ? 3 : 4;
int sev_pm25 =
id(pm25).state <= 10 ? 0 :
id(pm25).state <= 15 ? 1 :
id(pm25).state <= 25 ? 2 :
id(pm25).state <= 35 ? 3 :
id(pm25).state <= 55 ? 4 : 5;
int sev_pm10 =
id(pm10).state <= 20 ? 0 :
id(pm10).state <= 45 ? 2 :
id(pm10).state <= 90 ? 3 : 4;
int sev_voc =
id(voc).state <= 100 ? 0 :
id(voc).state <= 200 ? 2 :
id(voc).state <= 300 ? 3 : 4;
int sev_nox =
id(nox).state <= 50 ? 0 :
id(nox).state <= 100 ? 2 :
id(nox).state <= 200 ? 3 : 4;
int worst = sev_co2;
if (sev_pm25 > worst) worst = sev_pm25;
if (sev_pm10 > worst) worst = sev_pm10;
if (sev_voc > worst) worst = sev_voc;
if (sev_nox > worst) worst = sev_nox;
const char *estado[] = {
"BUENO",
"ACEPTABLE",
"MODERADO",
"REGULAR",
"MALO",
"MUY MALO"
};
lv_label_set_text(id(estado_aire), estado[worst]);
substitutions:
# Predefined Wi‑Fi access point SSID
wifi_ap: !secret wifi_ssid
# Predefined Wi‑Fi access point password
wifi_password: !secret wifi_password
# Fallback AP SSID used when main Wi‑Fi is not available
fallback_ap: "crowpanel-esp32-p4"
# Fallback AP password
fallback_ap_password: "CHANGE_ME"
# Default base URL for Home Assistant dashboards (used by Remote WebView)
ha_base_url: "http://192.168.1.30:8122/"
# Home Assistant API encryption key for this ESPHome device
ha_encryption_key: "Dp4Wgl1jj5F++6mnzSma7lHvgHCGfghlE/ohT96S+jw="
esphome:
name: advance-p4-10-1-inch
on_boot:
# Run after most components are initialized
priority: -100
then:
# Start inactivity timer (controls backlight & touch lock)
- script.execute: inactivity_timer
# Give network and API some time to come up
- delay: 3s
# Re‑apply last saved Remote WebView URL on boot
- lambda: |-
std::string full = id(rwv_url).state.c_str();
if (full.empty()) return;
// Strip scheme (http:// or https://)
std::string tmp = full;
auto pos = tmp.find("://");
if (pos != std::string::npos) tmp = tmp.substr(pos + 3);
// Extract host[:port] from host[:port]/path
std::string host_port;
auto slash = tmp.find('/');
host_port = (slash != std::string::npos) ? tmp.substr(0, slash) : tmp;
// Extract host only (drop port if present)
std::string host_only;
auto colon = host_port.find(':');
host_only = (colon != std::string::npos) ? host_port.substr(0, colon) : host_port;
// Use host from URL, but fixed Remote WebView port 8082
if (!host_only.empty()) {
id(rwv).set_server(host_only + ":8082");
}
// Set the target URL for Remote WebView
id(rwv).set_url(full);
external_components:
# Local custom components directory (Remote WebView implementation)
- source: components
refresh: 0s
components: [ remote_webview ]
esp32:
board: esp32-p4
framework:
type: esp-idf
# Enable PSRAM usage for code and read‑only data
sdkconfig_options:
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
CONFIG_SPIRAM_RODATA: y
# Extra IDF components used by Remote WebView
components:
- name: "espressif/esp_websocket_client"
ref: 1.5.0
- name: "bitbank2/jpegdec"
source: https://github.com/strange-v/jpegdec-esphome
# Serial logging configuration
logger:
baud_rate: 115200
hardware_uart: UART0 # Use UART0 for logs
level: INFO
# Native API for Home Assistant
api:
encryption:
key: $ha_encryption_key
# OTA updates over ESPHome
ota:
- platform: esphome
password: "bf1c7324cdcd0f41044c7722cbf5a048"
# ESP32‑Hosted Wi‑Fi6 module connection (ESP32‑C6 on the Waveshare board)
esp32_hosted:
variant: esp32c6
active_high: true
reset_pin: GPIO32
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO17
d1_pin: GPIO16
d2_pin: GPIO15
d3_pin: GPIO14
wifi:
# Main Wi‑Fi credentials from substitutions
ssid: $wifi_ap
password: $wifi_password
# Fallback AP (captive portal) when Wi‑Fi cannot connect
ap:
ssid: $fallback_ap
password: $fallback_ap_password
# Captive portal for initial configuration
captive_portal:
# PSRAM configuration (external RAM on ESP32‑P4)
psram:
mode: hex
speed: 200MHz
# Internal LDO regulator used to power some peripherals (e.g. display/touch)
esp_ldo:
- channel: 3
voltage: 2.5V
switch:
# Controls panel power for LCD and touch
- platform: gpio
pin: GPIO29
id: display_power
name: "Display Power"
restore_mode: ALWAYS_ON
# Amplifier power (active‑low on this board)
- platform: gpio
id: amp_power
name: "Speaker Power"
pin:
number: GPIO30
inverted: true
restore_mode: ALWAYS_ON
# Microphone routing switch (R/L channel selection on the board)
- platform: gpio
pin: GPIO20
id: mic_r_l
name: "Mic R-L"
restore_mode: ALWAYS_ON
output:
# LEDC PWM output for LCD backlight control
- platform: ledc
pin: GPIO31
id: backlight_pwm
frequency: 1000 Hz
light:
# Simple dimmable backlight light entity
- platform: monochromatic
output: backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
display:
# MIPI‑DSI display driver for Waveshare ESP32‑P4 7" LCD
- platform: mipi_dsi
id: my_display
model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B
reset_pin:
number: 41
rotation: 0
# Drawing controlled manually by Remote WebView
update_interval: never
auto_clear_enabled: false
dimensions:
width: 1024
height: 600
i2c:
# I²C bus for touch controller and other peripherals
- id: bus_a
sda: GPIO45
scl: GPIO46
frequency: 400kHz
touchscreen:
# GT911 capacitive touch controller on the Waveshare panel
platform: gt911
reset_pin: GPIO40
interrupt_pin: 42
transform:
mirror_x: false
mirror_y: false
# Any touch event wakes the backlight / restarts inactivity timer
on_touch:
then:
- script.execute: inactivity_timer
remote_webview:
# Remote WebView client that streams HA dashboard to the LCD
id: rwv
# Initial dummy server; will be replaced from rwv_url on boot
server: 127.0.0.1:8082
tile_size: 128
# Initial dummy URL; will be replaced from rwv_url on boot
url: http://127.0.0.1/
full_frame_tile_count: 40
max_bytes_per_msg: 32768
jpeg_quality: 85
every_nth_frame: 3 # el servidor procesa 1 de cada 2 frames
min_frame_interval: 150 # mínimo 150ms entre frames (~6fps)
i2s_audio:
# I2S bus for PDM microphone (clock only, data is on i2s_din_pin)
- id: i2s_mic
i2s_lrclk_pin: GPIO24
# I2S bus for external speaker amplifier
- id: i2s_spk
i2s_lrclk_pin: GPIO21
i2s_bclk_pin: GPIO22
microphone:
# PDM digital microphone connected via I2S
- platform: i2s_audio
id: pdm_mic
i2s_audio_id: i2s_mic
i2s_din_pin: GPIO26
adc_type: external
pdm: true
sample_rate: 16000
bits_per_sample: 16bit
speaker:
# External NS4168 I2S speaker amplifier
- platform: i2s_audio
id: ns4168_speaker
i2s_audio_id: i2s_spk
i2s_dout_pin: GPIO23
dac_type: external
sample_rate: 44100
bits_per_sample: 16bit
media_player:
# Media player abstraction on top of the I2S speaker
- platform: speaker
id: speaker_media
name: "Speaker"
announcement_pipeline:
speaker: ns4168_speaker
# If an announcement starts while mic is capturing, stop wake word engine
on_announcement:
- if:
condition:
- microphone.is_capturing:
then:
- script.execute: stop_wake_word
# When idle after announcement, restart wake word engine
on_idle:
- script.execute: start_wake_word
micro_wake_word:
# On‑device wake word detection using ESPHome micro_wake_word
on_wake_word_detected:
- logger.log: "Wake word detected"
- script.execute: inactivity_timer
- voice_assistant.start:
wake_word: !lambda return wake_word;
# Voice Activity Detection to suppress noise‑only events
vad:
model: vad
probability_cutoff: 0.85
sliding_window_size: 2
# Wake word models running locally
models:
- model: hey_jarvis
probability_cutoff: 0.8
sliding_window_size: 5
- model: okay_nabu
sliding_window_size: 5
probability_cutoff: 0.8
voice_assistant:
# ESPHome voice assistant integration with Home Assistant Assist
id: va
micro_wake_word: # bind to micro_wake_word component above
microphone:
microphone: pdm_mic
channels: 0
gain_factor: 1
media_player: speaker_media
noise_suppression_level: 2
# auto_gain: 31dBFS
on_end:
# Handle "nevermind" case with no TTS announcement
- wait_until:
condition:
- media_player.is_announcing:
timeout: 0.5s
# Restart only micro_wake_word if enabled; streaming wake words restart automatically
- if:
condition:
- lambda: |-
return id(wake_word_engine_location).current_option() == "On device";
then:
- wait_until:
- and:
- not:
voice_assistant.is_running:
- not:
speaker.is_playing:
- lambda: id(va).set_use_wake_word(false);
- micro_wake_word.start:
on_client_connected:
# Delay a bit so HA API is fully ready, then start wake word engine
- delay: 2s
- script.execute: start_wake_word
on_client_disconnected:
# Stop wake word when HA disconnects
- script.execute: stop_wake_word
button:
- platform: template
name: "Test Voice"
on_press:
- voice_assistant.start_continuous
text:
# Text entity used to configure Remote WebView URL from HA
- platform: template
id: rwv_url
name: "Remote WebView URL"
optimistic: true
restore_value: true # persist last URL locally in flash
mode: TEXT
min_length: 1
initial_value: $ha_base_url # default value if nothing stored yet
set_action:
- lambda: |-
std::string full = std::string(x.c_str());
// Strip scheme
std::string tmp = full;
auto pos = tmp.find("://");
if (pos != std::string::npos) {
tmp = tmp.substr(pos + 3);
}
// Extract host[:port]
std::string host_only;
auto slash = tmp.find('/');
std::string host_port = (slash != std::string::npos) ? tmp.substr(0, slash) : tmp;
// Drop port from HA URL, keep only hostname
auto colon = host_port.find(':');
if (colon != std::string::npos) {
host_only = host_port.substr(0, colon);
} else {
host_only = host_port;
}
// Configure Remote WebView server as <host>:8082
if (!host_only.empty()) {
std::string server = host_only + ":8082";
id(rwv).set_server(server);
ESP_LOGI("remote_webview", "Server set to: %s", server.c_str());
} else {
ESP_LOGW("remote_webview", "Failed to parse host from URL: %s", full.c_str());
}
// Apply URL to Remote WebView (immediately if connected, else queue)
if (!id(rwv).open_url(full)) {
id(rwv).set_url(full);
ESP_LOGI("remote_webview", "URL queued (not connected): %s", full.c_str());
}
select:
# Runtime switch: where wake word is handled (HA or on‑device)
- platform: template
entity_category: config
name: Wake word engine location
id: wake_word_engine_location
optimistic: true
restore_value: true
options:
- In Home Assistant
- On device
initial_option: In Home Assistant
on_value:
# Use streaming wake word in HA
- if:
condition:
lambda: return x == "In Home Assistant";
then:
- micro_wake_word.stop:
- delay: 500ms
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start_continuous:
# Use on‑device micro_wake_word
- if:
condition:
lambda: return x == "On device";
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop:
- delay: 500ms
- micro_wake_word.start:
script:
# Helper to start appropriate wake word engine depending on select state
- id: start_wake_word
then:
- if:
condition:
and:
- not:
- voice_assistant.is_running:
- lambda: |-
return id(wake_word_engine_location).current_option() == "On device";
then:
- lambda: id(va).set_use_wake_word(false);
- micro_wake_word.start:
- if:
condition:
and:
- not:
- voice_assistant.is_running:
- lambda: |-
return id(wake_word_engine_location).current_option() == "In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start:
# Helper to stop wake word engine depending on where it runs
- id: stop_wake_word
then:
- if:
condition:
lambda: |-
return id(wake_word_engine_location).current_option() == "In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop:
- if:
condition:
lambda: |-
return id(wake_word_engine_location).current_option() == "On device";
then:
- micro_wake_word.stop:
# Backlight / touch inactivity timer:
# - wakes display & enables touch on any activity
# - turns display off and disables touch after timeout
- id: inactivity_timer
mode: restart
then:
# If backlight is currently off, turn it on and re‑enable touch
- if:
condition:
light.is_off: back_light
then:
- light.turn_on:
id: back_light
- delay: 0.5s
- lambda: |-
id(rwv).disable_touch(false);
# Wait for inactivity duration
- delay: 15s
# Turn off backlight
- light.turn_off:
id: back_light
# Disable touch so screen does not react while "sleeping"
- lambda: |-
id(rwv).disable_touch(true);
# Example of LVGL usage; if LVGL is enabled, Remote WebView cannot be used
# lvgl:
# buffer_size: 100%
# byte_order: little_endian
#
# pages:
# - id: main_page
# widgets:
# - label:
# id: hello_label
# text: "Hello LVGL"
# align: CENTER
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment