Show who is at the door: camera images on the Guition (0.2.66)
An alert from esp_screens_show_alert takes a `camera` (a camera.* or image.* entity): a Guition with firmware 0.2.57 shows that moment's picture across the top of the card, and a tap opens the camera full screen. A camera or image tile opens the same full-screen view, refreshed every four seconds at a steady pace. ESP Screen Manager fetches the snapshot with its own access, sizes it for the screen and serves a 24-bit BMP under a random, short-lived link on port 8098; the screen loads it with ESPHome's online_image and holds no Home Assistant token. BMP because ESPHome decodes it per chunk, where a full-screen JPEG held the loop 0.6 s. The CYD keeps its alerts without the picture and gets no camera tiles. The Guition also builds with ESPHome 2026.8+ again: backlight_fade.h derived from ESPHome's LEDC output, which 2026.8 made final, so the backlight now dims through ESPHome's own light transition. Tested on the bench boards and a local add-on on the Home Assistant Yellow; docs/TEST_RESULTS_0266.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+138
-15
@@ -2,7 +2,7 @@
|
||||
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
|
||||
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.56"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.57"
|
||||
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
|
||||
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
|
||||
TOUCH_MOVE_LIMIT_PX: "0"
|
||||
@@ -69,6 +69,16 @@ substitutions:
|
||||
ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis
|
||||
ALERT_BUTTON_MAX: "16" # bytes on the OK button
|
||||
ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has
|
||||
# An alert with a camera image (firmware 0.2.57+): the image across the top of a taller card, the words under it.
|
||||
ALERT_CARD_H_IMAGE: "452"
|
||||
ALERT_IMAGE_INSET: "14"
|
||||
ALERT_IMAGE_SHIFT: "236" # the icon, title and subtitle move down by the image and a gap
|
||||
ALERT_SUBTITLE_H_IMAGE: "62" # two lines between the title and the button
|
||||
# Camera images (firmware 0.2.57+): the boxes ESP Screen Manager sizes a snapshot for (camera_feed.BOXES).
|
||||
CAMERA_FULL_W: "480"
|
||||
CAMERA_FULL_H: "480"
|
||||
CAMERA_THUMB_W: "392"
|
||||
CAMERA_THUMB_H: "220"
|
||||
OVERLAY_SLIDER_W: "144"
|
||||
OVERLAY_SLIDER_H: "300"
|
||||
OVERLAY_ICON_Y: "148"
|
||||
@@ -105,7 +115,6 @@ esphome:
|
||||
- <esphome/components/smart_display/cyd_ui.h>
|
||||
- <esphome/components/smart_display/light_controls.h>
|
||||
- <esphome/components/smart_display/guition_diagnostics.h>
|
||||
- <esphome/components/smart_display/backlight_fade.h>
|
||||
- <esphome/components/smart_display/alert_overlay.h>
|
||||
- <esphome/components/smart_display/settings_screen.h>
|
||||
name: ${DEVICE_NAME}
|
||||
@@ -197,12 +206,14 @@ esphome:
|
||||
theme::fill(id(paint_accent_bright), Paint::accent_bright);
|
||||
theme::fill(id(paint_button_dark), Paint::button_dark);
|
||||
theme::fill(id(paint_button_dark_pressed), Paint::button_dark_pressed);
|
||||
theme::fill(id(paint_camera), Paint::camera);
|
||||
};
|
||||
theme::paints();
|
||||
theme::set_dark(settings_screen::dark_mode != 0);
|
||||
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
|
||||
runtime_tiles::dismiss = []() {
|
||||
runtime_tiles::hide_detail();
|
||||
runtime_tiles::camera_close();
|
||||
id(active_entity).clear();
|
||||
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(id(color_detail_overlay), LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -210,6 +221,32 @@ esphome:
|
||||
lv_obj_add_flag(id(climate_mode_overlay), LV_OBJ_FLAG_HIDDEN);
|
||||
};
|
||||
runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); };
|
||||
// Camera images (firmware 0.2.57+): the two online_images and the alert's frame.
|
||||
// set_url() forgets the ETag, so it is only called for a new link: an unchanged picture then answers 304.
|
||||
runtime_tiles::camera_full.load = [](const std::string &url) {
|
||||
static std::string current;
|
||||
if (url != current) { id(camera_image).set_url(url); current = url; }
|
||||
id(camera_image).update();
|
||||
};
|
||||
runtime_tiles::camera_full.release = []() { id(camera_image).release(); };
|
||||
runtime_tiles::camera_full.source = []() { return id(camera_image).get_lv_image_dsc(); };
|
||||
runtime_tiles::camera_thumb.load = [](const std::string &url) {
|
||||
static std::string current;
|
||||
if (url != current) { id(alert_image).set_url(url); current = url; }
|
||||
id(alert_image).update();
|
||||
};
|
||||
runtime_tiles::camera_thumb.release = []() { id(alert_image).release(); };
|
||||
runtime_tiles::camera_thumb.source = []() { return id(alert_image).get_lv_image_dsc(); };
|
||||
runtime_tiles::alert_frame = id(alert_image_frame);
|
||||
runtime_tiles::alert_frame_icon = id(alert_image_icon);
|
||||
runtime_tiles::alert_room = [](bool image) {
|
||||
const int shift = image ? ${ALERT_IMAGE_SHIFT} : 0;
|
||||
lv_obj_set_height(id(alert_card), image ? ${ALERT_CARD_H_IMAGE} : ${ALERT_CARD_H});
|
||||
lv_obj_set_y(id(alert_icon), ${ALERT_ICON_Y} + shift);
|
||||
lv_obj_set_y(id(alert_title), ${ALERT_TITLE_Y} + shift);
|
||||
lv_obj_set_y(id(alert_subtitle), ${ALERT_SUBTITLE_Y} + shift);
|
||||
lv_obj_set_height(id(alert_subtitle), image ? ${ALERT_SUBTITLE_H_IMAGE} : ${ALERT_SUBTITLE_H});
|
||||
};
|
||||
runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); };
|
||||
runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl));
|
||||
runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl));
|
||||
@@ -285,7 +322,7 @@ esphome:
|
||||
settings_screen::restart_device = []() { ESP_LOGI("settings", "restart from the screen"); App.safe_reboot(); };
|
||||
// A dimmed screen, a calibration or an alert owns the glass; the page waits.
|
||||
settings_screen::may_open = []() {
|
||||
return runtime_tiles::enabled && !id(display_dimmed) && !id(calibration_active) && !id(alert_active);
|
||||
return runtime_tiles::enabled && !id(display_dimmed) && !id(calibration_active) && !id(alert_active) && !runtime_tiles::camera_visible();
|
||||
};
|
||||
settings_screen::drift = []() { return cyd::touch_guard.distance(); };
|
||||
settings_screen::before_open = []() { id(close_cards).execute(); };
|
||||
@@ -407,6 +444,20 @@ api:
|
||||
}
|
||||
runtime_tiles::show_detail(index);
|
||||
}
|
||||
# A camera full screen from Home Assistant (firmware 0.2.57+), as a tap on its tile opens it; an empty entity closes
|
||||
# it. Only a camera on the screen's tiles or in a recent alert gets an image from ESP Screens.
|
||||
- action: preview_camera
|
||||
variables:
|
||||
entity: string
|
||||
then:
|
||||
- lambda: |-
|
||||
if (entity.empty()) { runtime_tiles::camera_close(); return; }
|
||||
id(last_use_ms) = millis();
|
||||
id(wake_display).execute();
|
||||
std::string name = entity;
|
||||
for (size_t i = 0; i < runtime_tiles::model.count; ++i)
|
||||
if (runtime_tiles::model.tiles[i].entity == entity) { name = runtime_tiles::model.tiles[i].name; break; }
|
||||
runtime_tiles::camera_open(entity, name);
|
||||
# The settings page from Home Assistant: `page` 0 is its menu, 1-4 open a group straight away
|
||||
# (Brightness, Night, Screen, This screen) and a negative page closes it and goes back to page 1.
|
||||
# Handy for support ("look at Screen -> Rotation") and for the render tests.
|
||||
@@ -504,10 +555,43 @@ wifi:
|
||||
|
||||
captive_portal:
|
||||
|
||||
# Camera images (firmware 0.2.57+): ESP Screen Manager serves each image already sized for this screen, as an uncompressed
|
||||
# BMP on its own port on the LAN (screen_manager/app/camera_feed.py). ESPHome decodes a BMP piece by piece as it arrives,
|
||||
# 4 KB per loop, where a JPEG of the full screen held the loop for 0.6 s; touch and drawing keep their turn. One image full
|
||||
# screen and one in an alert's frame; runtime_tiles.h decides when they load and frees them when they close. Plain HTTP
|
||||
# on the LAN, no certificates.
|
||||
http_request:
|
||||
timeout: 4s
|
||||
verify_ssl: false
|
||||
|
||||
# The top-level online_image form that ESPHome 2026.6.2 (min_version) knows; 2026.7+ still reads it and suggests
|
||||
# `image: - platform: online_image`, which this profile can use once min_version reaches 2026.7.
|
||||
online_image:
|
||||
- id: camera_image
|
||||
url: "http://127.0.0.1/camera.bmp" # set when a camera opens
|
||||
format: BMP
|
||||
type: RGB565
|
||||
buffer_size: 4096
|
||||
on_download_finished:
|
||||
- lambda: 'runtime_tiles::camera_loaded(false, cached);'
|
||||
on_error:
|
||||
- lambda: 'runtime_tiles::camera_failed(false);'
|
||||
- id: alert_image
|
||||
url: "http://127.0.0.1/camera.bmp" # set when an alert brings an image
|
||||
format: BMP
|
||||
type: RGB565
|
||||
buffer_size: 4096
|
||||
on_download_finished:
|
||||
- lambda: 'runtime_tiles::camera_loaded(true, cached);'
|
||||
on_error:
|
||||
- lambda: 'runtime_tiles::camera_failed(true);'
|
||||
|
||||
interval:
|
||||
- interval: 250ms
|
||||
then:
|
||||
- lambda: runtime_tiles::tick();
|
||||
- lambda: |-
|
||||
runtime_tiles::tick();
|
||||
runtime_tiles::camera_tick();
|
||||
- interval: 1s
|
||||
then:
|
||||
- if:
|
||||
@@ -1306,7 +1390,8 @@ touchscreen:
|
||||
if (!step) continue;
|
||||
const char *blocked = !runtime_tiles::enabled ? "no runtime tiles" : !runtime_tiles::swipe_pages ? "setting off"
|
||||
: id(display_dimmed) ? "screen dimmed" : id(calibration_active) ? "calibration" : !id(active_entity).empty() ? "card open"
|
||||
: (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detail card open" : nullptr;
|
||||
: (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detail card open"
|
||||
: runtime_tiles::camera_visible() ? "camera open" : nullptr;
|
||||
if (blocked) { ESP_LOGI("touch", "edge swipe ignored: %s", blocked); continue; }
|
||||
cyd::touch_guard.consume();
|
||||
for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev);
|
||||
@@ -1473,6 +1558,7 @@ lvgl:
|
||||
- id: paint_accent_bright
|
||||
- id: paint_button_dark
|
||||
- id: paint_button_dark_pressed
|
||||
- id: paint_camera
|
||||
|
||||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||||
@@ -1542,6 +1628,39 @@ lvgl:
|
||||
text_font: sublabel_big
|
||||
styles: paint_ink_soft
|
||||
clickable: false
|
||||
# The alert's camera image (firmware 0.2.57+) across the top of the card, shown when the app announced
|
||||
# one; a tap opens the camera full screen over the alert. runtime_tiles.h puts the image in it.
|
||||
- obj:
|
||||
id: alert_image_frame
|
||||
x: ${ALERT_IMAGE_INSET}
|
||||
y: ${ALERT_IMAGE_INSET}
|
||||
width: ${CAMERA_THUMB_W}
|
||||
height: ${CAMERA_THUMB_H}
|
||||
hidden: true
|
||||
styles: paint_camera
|
||||
bg_opa: COVER
|
||||
radius: 0
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
pressed:
|
||||
bg_opa: COVER
|
||||
on_click:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return cyd::touch_guard.accept(millis(), 14) && !runtime_tiles::alert_camera.empty();'
|
||||
then:
|
||||
- lambda: 'runtime_tiles::camera_open(runtime_tiles::alert_camera, id(alert_title_text));'
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_image_icon
|
||||
align: CENTER
|
||||
text: "\U000F07AE"
|
||||
text_font: materialdesign_icons
|
||||
clickable: false
|
||||
- obj:
|
||||
id: alert_ok
|
||||
align: BOTTOM_RIGHT
|
||||
@@ -1591,6 +1710,11 @@ lvgl:
|
||||
height: 2
|
||||
spin_time: 1s
|
||||
arc_length: 60deg
|
||||
# Never shown either: it makes ESPHome compile LVGL's image widget, which a camera image is drawn with.
|
||||
- image:
|
||||
id: camera_image_seed
|
||||
hidden: true
|
||||
src: alert_image
|
||||
- label:
|
||||
id: lbl_room
|
||||
x: 16
|
||||
@@ -3619,7 +3743,7 @@ script:
|
||||
auto now = id(homeassistant_time).now();
|
||||
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
|
||||
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
|
||||
// Standby dims slowly; the tile redraws below no longer interrupt the hardware fade.
|
||||
// Standby dims slowly (ESPHome's transition); waking is quick.
|
||||
id(set_backlight).execute(level, id(display_dimmed) ? 1500 : 300);
|
||||
if (id(display_dimmed)) { lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(id(dim_wake_overlay)); }
|
||||
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -3669,7 +3793,7 @@ script:
|
||||
lambda: |-
|
||||
if (!runtime_tiles::auto_home || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||||
if (id(display_dimmed)) return false;
|
||||
bool away = settings_screen::visible() || !id(active_entity).empty() || id(tile_page) != 0 ||
|
||||
bool away = settings_screen::visible() || runtime_tiles::camera_visible() || !id(active_entity).empty() || id(tile_page) != 0 ||
|
||||
(runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN));
|
||||
return away && (millis() - id(last_use_ms)) > (uint32_t) runtime_tiles::auto_home_seconds * 1000UL;
|
||||
then:
|
||||
@@ -3694,6 +3818,8 @@ script:
|
||||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||||
// An alert is the one thing that outranks the settings page, so that closes first.
|
||||
settings_screen::close();
|
||||
// An open camera closes; the card makes room for an image the app announced for this alert.
|
||||
runtime_tiles::alert_prepare();
|
||||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||||
id(alert_active) = true;
|
||||
id(alert_title_text) = alert.title;
|
||||
@@ -3733,6 +3859,7 @@ script:
|
||||
- lambda: |-
|
||||
id(alert_active) = false;
|
||||
id(last_touch_ms) = millis();
|
||||
runtime_tiles::alert_clear();
|
||||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
@@ -3789,20 +3916,16 @@ script:
|
||||
- lvgl.widget.hide: dim_wake_overlay
|
||||
- script.execute: close_cards
|
||||
|
||||
# One owner for the backlight level: the LEDC fader steps the duty itself, so
|
||||
# LVGL redraws cannot make the transition stutter. A new call interrupts the
|
||||
# running fade from its current duty; afterwards ESPHome's light state is synced.
|
||||
# One owner for the backlight level, through ESPHome's own light transition (firmware 0.2.57+). Firmware 0.2.13-0.2.56
|
||||
# stepped the duty with the LEDC hardware fader by reaching into ESPHome's LEDC output, which ESPHome 2026.8 made final;
|
||||
# a new call interrupts the running transition from where it is, as ESPHome does for any light.
|
||||
- id: set_backlight
|
||||
mode: restart
|
||||
parameters:
|
||||
level: int
|
||||
ms: int
|
||||
then:
|
||||
- lambda: |-
|
||||
if (!backlight_fade::start(*id(gpio_backlight_pwm), *id(back_light), level / 100.0f, ms))
|
||||
id(back_light).turn_on().set_transition_length(ms).set_brightness(level / 100.0f).perform();
|
||||
- delay: !lambda "return ms + 40;"
|
||||
- lambda: backlight_fade::sync(*id(gpio_backlight_pwm), *id(back_light), level / 100.0f);
|
||||
- lambda: id(back_light).turn_on().set_transition_length(ms).set_brightness(level / 100.0f).perform();
|
||||
|
||||
# Standby: the standby time ran out (the 1 s interval checks Auto standby) or Sleep was pressed.
|
||||
# The settings page and every card close; with "also on standby" the screen goes back to page 1 too.
|
||||
|
||||
Reference in New Issue
Block a user