Files
homeassistant_espscreen/screen_manager/app/static/index.html
T
Max Gramser dfd1a26d69 Let Wake only light the screen, and open ESP Screens without a chosen screen (0.2.65)
Wake from Home Assistant counted as a tap: it reset last_touch_ms, the one
clock that standby and Back to page 1 both read. An automation pressing Wake
on every motion therefore held an open card or page 2 up for as long as
someone moved in the room. Back to page 1 now has its own clock, last_use_ms,
written only by the boot, the touchscreen triggers, open_settings from Home
Assistant and the UI self test. Wake, an alert ending and Auto standby on
still restart the standby time, and Wake still brings back the brightness and
the second hand.

The rule moved from the interval into the script back_to_page_1_when_due.
Wake runs it right after waking a dimmed screen, so a page whose time ran out
during standby lights up on page 1 instead of jumping there a second later.

ESP Screens no longer opens the first screen by itself. Until one is picked
the page says "Choose a screen", and with no screens it still offers the
install; both cards start hidden so neither flashes while the page loads.

Checked with 48 host API checks per board, both ESP32 builds, 314 Python and
16 C++ tests, and the editor on the demo home. Firmware 0.2.56.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:41:19 +02:00

596 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ESP Screens</title>
<link rel="stylesheet" href="static/style.css" />
<script src="static/app.js" defer></script>
</head>
<body>
<header>
<div class="brand">
<span class="mark">▦</span>
<div>
<strong>ESP Screens</strong
><small>Tiles for your CYD and Guition, without reflashing</small>
</div>
</div>
<nav class="toolbar" aria-label="Actions">
<button id="open-settings" class="tool" type="button" aria-controls="settings-view">
<span class="tool-icon" id="open-settings-icon" aria-hidden="true">⚙</span>
<span class="tool-text"
><b id="open-settings-label">Settings</b
><small id="open-settings-hint">New screen, updates, alerts, Claude</small></span
>
</button>
</nav>
</header>
<main>
<div id="home-view">
<div class="intro">
<div>
<p class="eyebrow">HOME ASSISTANT</p>
<h1>Manage your remote screens here.</h1>
</div>
<span id="connection" role="status" class="badge">Connecting…</span>
</div>
<div class="workspace">
<aside>
<h2>My screens</h2>
<div id="screens"></div>
<div id="pending"></div>
<button class="quiet" id="refresh">↻ Refresh</button>
</aside>
<!-- Hidden until the first inventory, so neither card flashes while the page loads. -->
<section id="choose" class="empty" hidden>
<span class="empty-icon">▦</span>
<h2>Choose a screen</h2>
<p>Pick one of your screens to change its tiles, top bar and settings.</p>
</section>
<section id="empty" class="empty" hidden>
<span class="empty-icon">▦</span>
<h2>Start with a screen</h2>
<p>
Connect a CYD or Guition via USB to your Home Assistant machine.
Profile, firmware, and the first flash happen in one window.
</p>
<button id="start">Install new screen</button>
</section>
<section id="editor" hidden>
<div class="editor-head">
<div>
<p class="eyebrow" id="screen-name"></p>
<h2>Make it your screen</h2>
</div>
<div class="editor-head-actions">
<button id="open-override" class="quiet mini" type="button">Override YAML</button>
<span id="delivery" class="badge"></span>
</div>
</div>
<section class="topbar-editor" id="topbar" aria-labelledby="topbar-title">
<div class="section-title">
<h3 id="topbar-title">Top bar</h3>
<span id="topbar-count"></span>
</div>
<div class="topbar-fields">
<label class="topbar-name"
>Left: name<input
id="title"
maxlength="60"
placeholder="For example: Living room"
/></label>
<div class="topbar-right">
<span class="topbar-caption" id="topbar-caption">Right: time, sensors, or a clock</span>
<div
id="topbar-chips"
class="topbar-chips"
role="list"
aria-labelledby="topbar-caption"
></div>
</div>
</div>
<p class="hint" id="topbar-hint"></p>
</section>
<div class="columns" id="tile-section">
<div class="tile-editor">
<div class="section-title">
<h3>Your tiles</h3>
<span id="count">0 / 10</span>
</div>
<div id="layout-preview" aria-label="Screen layout"></div>
<button id="add-page" class="quiet add-page" type="button">
+ Add page
</button>
<p id="no-tiles" class="hint">
Add your first light, scene, or device via the picker.
</p>
<p id="positions-hint" class="hint" hidden></p>
</div>
<div class="picker">
<label for="search">Add an entity</label
><input
id="search"
type="search"
placeholder="Search by name, device, or room…"
autocomplete="off"
/>
<div class="filters" id="filters"></div>
<div id="results" aria-live="polite"></div>
</div>
</div>
<section class="screen-settings" id="general-settings" aria-labelledby="settings-heading">
<div class="section-title settings-title">
<h3 id="settings-heading">Screen settings</h3>
<span class="hint" id="settings-status" role="status"></span>
</div>
<div class="settings-cards" id="settings-groups"></div>
</section>
<details class="screen-settings" id="inspector-section">
<summary>
Inspector
<span class="hint"
>Connection, status, and supported properties</span
>
</summary>
<button id="inspect" class="quiet">Read current data</button>
<div id="inspection-summary"></div>
<pre id="inspection">Select a screen and read the data.</pre>
</details>
<footer class="savebar">
<div>
<strong id="dirty">All saved</strong
><small id="save-detail"
>Changes apply without reflashing.</small
>
</div>
<button id="save">Save & send to screen</button>
</footer>
</section>
</div>
</div>
<section id="settings-view" class="settings-view" hidden aria-labelledby="settings-title">
<div class="intro">
<div>
<p class="eyebrow">SETTINGS</p>
<h1 id="settings-title">Everything around your screens.</h1>
</div>
<button type="button" class="quiet" id="close-settings">← My screens</button>
</div>
<div class="settings-groups">
<section class="settings-group" aria-labelledby="settings-screens-title">
<h2 id="settings-screens-title">Screens</h2>
<p class="hint">Add a screen, or build and reinstall the firmware of one you already have.</p>
<div class="settings-tools">
<button id="new-screen" class="tool tool-primary" type="button">
<span class="tool-icon" aria-hidden="true">+</span>
<span class="tool-text"
><b>New screen</b><small>Connect and install</small></span
>
</button>
<button id="open-firmware" class="tool" type="button">
<span class="tool-icon" aria-hidden="true">⇪</span>
<span class="tool-text"
><b>Firmware & USB</b
><small>Build and install via USB or Wi-Fi</small></span
>
</button>
</div>
</section>
<section class="settings-group updates" id="updates" aria-labelledby="updates-title">
<h2 id="updates-title">Firmware updates</h2>
<p class="hint" id="updates-hint"></p>
<button id="update-all" hidden>Update all screens</button>
<label class="check">
<input type="checkbox" id="auto-update" />
<span
>Update automatically every night<small
>Between 03:00 and 06:00, one screen at a time. Stops on an
error and reports it in Home Assistant.</small
></span
>
</label>
</section>
<section class="settings-group" aria-labelledby="settings-alerts-title">
<h2 id="settings-alerts-title">Alerts</h2>
<p class="hint">
A card over the whole screen from an automation, on one screen or on all of them.
The cheatsheet has the action names, fields, icons, and colors.
</p>
<div class="settings-tools">
<button id="open-alerts" class="tool" type="button">
<span class="tool-icon" aria-hidden="true">!</span>
<span class="tool-text"
><b>Alerts</b
><small>Full-screen notifications, from an automation</small></span
>
</button>
</div>
</section>
<section class="settings-group" id="claude" aria-labelledby="claude-title">
<h2 id="claude-title">Claude</h2>
<p class="hint">
Teach Claude about ESP Screens, then ask: "Show an alert on all my screens when the
mailbox is full." Claude knows the alert events, every field, color, and icon.
</p>
<div class="claude-skill">
<button type="button" id="claude-install">Install for Claude Code</button>
<span id="claude-status" class="hint" role="status"></span>
</div>
<small>
For Claude Code in Home Assistant. Writes one file to
<code id="claude-path">/homeassistant/.claude/skills/esp-screens</code>; installing
again replaces it.
</small>
<div class="claude-skill">
<a class="button again" id="claude-download" href="api/claude-skill.zip" download="esp-screens.zip">Download for claude.ai</a>
<small>Upload the zip in Claude under Customize → Skills.</small>
</div>
</section>
</div>
</section>
</main>
<dialog id="installer">
<form id="install-form">
<div class="dialog-head">
<div>
<p class="eyebrow">NEW SCREEN</p>
<h2 id="install-title">Connect and install.</h2>
</div>
<button
type="button"
class="quiet"
id="close-install"
aria-label="Close"
>
✕
</button>
</div>
<div id="install-setup">
<p>
Connect the screen with a USB data cable to the machine running Home
Assistant. ESP Screens creates its own profile with unique
keys, builds the firmware, and puts it straight on the screen.
</p>
<fieldset>
<legend>Which screen do you have?</legend>
<label class="board"
><input type="radio" name="board" value="cyd" checked /><span
><b>CYD · 2.8 inch</b
><small>ESP32-2432S028 · 320 × 240</small></span
></label
><label class="board"
><input type="radio" name="board" value="guition" /><span
><b>Guition · 4 inch</b
><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span
></label
>
</fieldset>
<label
>Name<input
name="friendly_name"
required
maxlength="60"
placeholder="Living room screen"
autocomplete="off" /><small class="node-line"
>Device name <code id="node-preview">screen</code
><button type="button" class="link" id="edit-node">
customize
</button></small
><small
>Every screen gets its own profile with its own keys. Four
screens? Go through this four times with a different name.</small
></label
>
<label id="node-label" hidden
>Device name<input
name="name"
pattern="[a-z][a-z0-9-]{0,29}"
maxlength="30"
autocomplete="off" /><small
>Lowercase letters, digits, and dashes. A different name for
each screen.</small
></label
>
<fieldset id="wifi-fields" class="wifi" hidden disabled>
<legend>Wi-Fi</legend>
<p class="hint" id="wifi-status"></p>
<label id="wifi-ssid-label"
>Wi-Fi name (2.4 GHz)<input name="wifi_ssid" autocomplete="off"
/></label>
<label id="wifi-password-label"
>Wi-Fi password<input
name="wifi_password"
type="password"
autocomplete="new-password"
/></label>
</fieldset>
<label
>Install via<select name="target" id="install-target"></select
><small id="target-hint"></small
></label>
<p class="hint" id="install-note"></p>
<div class="install-actions">
<button type="submit" id="install-go">Install</button>
<span id="install-status" role="status"></span>
</div>
</div>
<div id="install-progress" hidden>
<div class="progress-head">
<span class="spin" id="progress-spin"></span>
<span class="outcome" id="progress-mark" hidden></span>
<strong id="progress-title"></strong>
</div>
<p id="progress-detail"></p>
<div id="install-result" hidden>
<div class="key-box">
<span>API key</span><code id="api-key"></code>
<button type="button" class="mini" id="copy-key">Copy</button>
</div>
<ol class="steps" id="install-steps"></ol>
</div>
<details id="install-log-wrap">
<summary>ESPHome log</summary>
<pre id="install-log"></pre>
</details>
<div class="install-actions">
<button type="button" id="install-retry" hidden>
Retry
</button>
<button type="button" class="quiet" id="install-close">
Close
</button>
</div>
</div>
</form>
</dialog>
<dialog id="firmware-dialog">
<div class="dialog-head">
<div>
<p class="eyebrow">FIRMWARE & USB</p>
<h2>Build and install screen firmware</h2>
</div>
<button id="close-firmware" class="quiet" aria-label="Close">✕</button>
</div>
<p>
The built-in ESPHome CLI uses your own profile (YAML with keys)
and secrets. Choose the screen's profile, then a USB port (screen
connected to the Home Assistant machine) or <b>Wi-Fi / OTA</b> with the hostname.
<b>Check</b> only validates, <b>Build only</b> compiles,
<b>Build & install</b> puts the firmware on the screen. Your tiles
stay as they are in ESP Screens. The first build takes a few minutes.
</p>
<label>ESPHome profile<select id="firmware-file"></select></label>
<label
>Install to<select id="firmware-port">
<option value="ota">Wi-Fi / OTA</option>
</select></label
>
<label id="firmware-host-label"
>IP address or hostname<input
id="firmware-host"
placeholder="screen-livingroom.local"
/></label>
<div class="yaml-actions">
<button id="firmware-validate" class="quiet">Check</button
><button id="firmware-build" class="quiet">Build only</button
><button id="firmware-install">Build & install</button>
</div>
<p id="firmware-status" role="status"></p>
<pre id="firmware-log"></pre>
</dialog>
<dialog id="override-dialog" class="override-dialog" aria-labelledby="override-title">
<div class="dialog-head">
<div>
<p class="eyebrow">ADVANCED · LOCAL OVERRIDE</p>
<h2 id="override-title">Override YAML</h2>
</div>
<button id="close-override" class="quiet" type="button" aria-label="Close">✕</button>
</div>
<p class="override-intro">
This small file is yours. It is loaded after the shared screen package and stays in place when
ESP Screen Manager updates. Change hardware-specific details here, such as a display model.
</p>
<div class="override-notice">
<span class="override-icon" aria-hidden="true">⌘</span>
<div>
<strong>What is safe to change?</strong>
<span>Display, touchscreen, substitutions, pins and other device details. Name, Wi-Fi, API, OTA and package links remain managed.</span>
</div>
</div>
<div class="override-file-row">
<span id="override-file" class="override-file"></span>
<span id="override-state" class="badge"></span>
</div>
<div class="yaml-editor" id="yaml-editor-wrap">
<div id="override-gutter" class="yaml-gutter" aria-hidden="true">1</div>
<textarea id="override-editor" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"
aria-label="Local override YAML" placeholder="# Example\ndisplay:\n - id: !extend my_display\n model: ST7789V"></textarea>
</div>
<div class="override-tools">
<button id="override-example" class="quiet mini" type="button">Use example</button>
<button id="override-empty" class="quiet mini" type="button">Clear</button>
<span id="override-count" class="hint"></span>
</div>
<p id="override-status" class="override-status" role="status"></p>
<div class="yaml-actions override-actions">
<button id="override-check" class="quiet" type="button">Save & check</button>
<button id="override-save" type="button">Save override</button>
</div>
</dialog>
<dialog id="tile-sheet" class="tile-sheet" aria-label="Configure tile"></dialog>
<dialog id="topbar-sheet" class="tile-sheet" aria-label="Top bar"></dialog>
<dialog id="alerts-dialog" class="alerts" aria-label="Alerts">
<div class="dialog-head">
<div>
<p class="eyebrow">ALERTS · CHEATSHEET</p>
<h2>A full-screen notification, from an automation.</h2>
</div>
<button id="close-alerts" class="quiet" aria-label="Close">✕</button>
</div>
<p>
Every screen has the <code>show_alert</code> action. It places a card over everything,
wakes the screen, and keeps the backlight at normal brightness until someone taps
the button or the timeout runs out. Home Assistant itself shows no explanation or
dropdowns for ESPHome actions; everything you can fill in is listed here.
</p>
<div class="alert-hero">
<div class="alert-mock" id="alert-mock" aria-hidden="true">
<div class="alert-mock-card">
<span class="mdi alert-mock-icon" id="alert-mock-icon"></span>
<strong id="alert-mock-title">Someone is at the door</strong>
<span id="alert-mock-subtitle">Door 3, back</span>
<button type="button" id="alert-mock-button" tabindex="-1">Coming</button>
</div>
</div>
<nav class="alerts-nav" aria-label="Sections">
<button type="button" data-jump="alerts-screens">Your screens</button>
<button type="button" data-jump="alerts-howto">In an automation</button>
<button type="button" data-jump="alerts-all">All screens</button>
<button type="button" data-jump="alerts-fields">The fields</button>
<button type="button" data-jump="alerts-icons">Icons</button>
<button type="button" data-jump="alerts-colors">Colors</button>
<button type="button" data-jump="alerts-behaviour">Behavior</button>
<button type="button" data-jump="alerts-events">Events</button>
<button type="button" data-jump="alerts-tips">Tips</button>
</nav>
</div>
<section id="alerts-screens">
<h3>Your screens</h3>
<p>
Every screen has its own action, with the device name in it. From firmware
<code id="alerts-min-firmware">0.2.31</code>; update an older screen with its
Update button under My screens.
</p>
<div id="alerts-screen-list"></div>
</section>
<section id="alerts-howto">
<h3>In an automation</h3>
<ol class="steps">
<li><b>Add action</b> and search for <b>ESPHome</b>. Choose your screen's action:
Home Assistant calls it "Performs the action show_alert of the node
<i>device name</i>".</li>
<li><b>Fill in the fields.</b> Home Assistant asks for all seven; leave what you don't
use empty (<code>""</code>, <code>0</code>, off).</li>
<li>Prefer YAML? Choose <b>Edit in YAML</b> on the action and paste the example
below.</li>
</ol>
<div class="alert-example">
<label
>Example for<select id="alerts-example-screen"></select
></label>
<div class="copy-line">
<pre id="alerts-example"></pre>
<button type="button" class="mini" id="alerts-example-copy">Copy YAML</button>
</div>
</div>
</section>
<section id="alerts-all">
<h3>All screens at once</h3>
<p>
Fire the event <code id="alerts-all-event">esp_screens_show_alert</code> with the same
fields, and ESP Screens passes the alert on to every screen that is online, including
screens you add later. <code id="alerts-all-dismiss">esp_screens_dismiss_alert</code>
clears it everywhere. In an automation, add the <b>Event</b> action or paste the example
under <b>Edit in YAML</b>. Leave out the fields you don't use. ESP Screens has to be
running for this.
</p>
<div class="copy-line">
<pre id="alerts-all-example"></pre>
<button type="button" class="mini" id="alerts-all-copy">Copy YAML</button>
</div>
</section>
<section id="alerts-fields">
<h3>The fields</h3>
<p>
Text is free-form; the limit is in bytes, and an accented letter counts as two.
What doesn't fit gets an ellipsis (title) or is dropped (subtitle).
</p>
<div class="table-scroll"><table id="alerts-field-table"></table></div>
</section>
<section id="alerts-icons">
<h3>Icons</h3>
<p>
Fill in the name in <code>icon</code>, for example <code>doorbell</code>.
<code>mdi:doorbell</code> and the hex codepoint (<code>F12E6</code>) also work.
Only these icons are included in the firmware; any other name shows the
warning triangle. Tap an icon to copy its name.
</p>
<p class="alerts-subhead">Handy for notifications</p>
<div class="chips" id="alerts-suggested"></div>
<label class="alerts-search"
>Search for an icon<input
id="alerts-icon-search"
type="search"
placeholder="Search by name or description…"
autocomplete="off"
/></label>
<div id="alerts-icon-groups"></div>
</section>
<section id="alerts-colors">
<h3>Colors</h3>
<p>
Fill in the name in <code>color</code>. These are the pastel shades of the tiles,
with dark text. Empty or unknown gives the white card. Tap to copy.
</p>
<div class="swatches" id="alerts-swatches"></div>
</section>
<section id="alerts-behaviour">
<h3>Behavior</h3>
<ul class="alerts-list">
<li><b>Wakes the screen.</b> An alert immediately wakes a dimmed or dark screen and
appears above every page, every open light card, and the standby layer.</li>
<li><b>On until the button.</b> As long as the card is showing, the backlight stays at
normal brightness. Standby and night hours wait. After the button, the regular
standby time starts again.</li>
<li><b>Timeout.</b> <code>timeout: 0</code> waits for the button, however long that
takes. With <code>timeout: 60</code> the card disappears after a minute, but the
button closes it sooner.</li>
<li><b>Blinking.</b> <code>flash: true</code> blinks the backlight four times
when the alert arrives (about a second) and leaves the screen on afterward.</li>
<li><b>Replacing.</b> A new alert replaces the current one; there's no stack.
The replaced alert reports itself as <code>replaced</code>.</li>
<li><b>Closing remotely.</b> <code>dismiss_alert</code> clears the card,
for example if the door is already open. Without an alert, that action does nothing.</li>
<li><b>Tapping beside the card</b> does nothing; only the button closes it. That way a
wake-up tap never accidentally dismisses the alert.</li>
</ul>
</section>
<section id="alerts-events">
<h3>Events</h3>
<p>
Every end fires the event <code id="alerts-event-name">esphome.screen_alert</code>
with <code>action</code>, <code>title</code>, <code>screen</code> (the device name)
and the <code>device_id</code> that Home Assistant adds. Visible under
Settings → Tools → Events.
</p>
<div class="table-scroll"><table id="alerts-ending-table"></table></div>
<p class="alerts-subhead">Waiting until someone presses the button</p>
<div class="copy-line">
<pre id="alerts-wait-example"></pre>
<button type="button" class="mini" id="alerts-wait-copy">Copy YAML</button>
</div>
</section>
<section id="alerts-tips">
<h3>Tips</h3>
<ul class="alerts-list">
<li>Use a <b>color per kind of notification</b>: orange for the door, red for
smoke or water, green for "done". The text always stays dark and readable.</li>
<li>Keep the title <b>short</b>; a screen shows about twenty characters on one line and
ends a longer title with an ellipsis. Put details in the subtitle.</li>
<li>A <b>custom button text</b> turns the notification into a question: "Coming", "Seen",
"Turn off". The event then tells you who responded.</li>
<li>For an alert that resolves itself (the door opens, the laundry gets taken out)
close it in the same automation with <code>dismiss_alert</code>.</li>
<li>Every screen? Fire <code>esp_screens_show_alert</code> once (see All screens at
once). The screens still work independently and each reports with its own
<code>screen</code>.</li>
<li>Rather <b>ask Claude</b> for the automation? Install the ESP Screens skill under
Settings → Claude.</li>
</ul>
</section>
</dialog>
<div id="toast" role="status" hidden></div>
</body>
</html>