-
The knob is NOT a quadrature encoder. A PCNT rotary_encoder reads zero.
Direction is encoded by which pin pulses: left → GPIO2, right → GPIO1, one
clean LOW pulse per detent. Read two independent binary_sensor on_press instead.
The knob can't be pressed (GPIO0 is BOOT) - trigger actions with a screen tap.
-
Strapping-pin warnings are expected and harmless. ESPHome warns (unconditionally,
no per-pin suppress) about GPIO0 (WS2812 ring), GPIO3 (DAC BCK), GPIO45
(DAC WS/LRCK) and GPIO46 (DAC MUTE) - all are ESP32-S3 strapping pins used per the
vendor pinout. They work fine after boot; ignore the warnings. Separately, silence
gfonts "missing glyphs" warnings with ignore_missing_glyphs: true (GF_Latin_Core lists
combining accents the static Roboto face lacks), or list exact glyphs: for pixel/icon fonts.
-
16 MB partitions → first flash MUST be over USB. partitions.csv uses big app
slots (~7.9 MB). A partition-table change can't be applied via OTA; flash once over
USB, then OTA works.
-
LVGL performance is tight on this S3 + QSPI panel.
- Keep
lvgl: buffer_size: 24% (puts the draw buffer in fast internal RAM instead
of slow PSRAM at the default 100%).
- Keep
display: data_rate: 80MHz.
- A rotating full-screen HUD image and an animated radial equalizer were both tried
and removed as too laggy. Add heavy full-screen redraws only very carefully.
logger: level: INFO - DEBUG is very noisy during voice and adds overhead.
-
Lambda string/symbol pitfalls (LVGL labels):
- For MDI/Unicode glyphs in C++ lambdas use the actual UTF-8 bytes or
chr(0xF....)
style - declare each glyph in the font: glyphs: list or it renders as tofu.
\n inside a lambda works inline ('...\n...'); inside a block scalar (|-)
a literal newline breaks YAML. Keep multi-line label text on one quoted line.
- Montserrat has no
-; guard "no data yet" cases with an empty string, not a dash.
-
Album art (online_image) decodes fine but the LVGL widget won't redraw by
itself - you must call lvgl.image.update in on_download_finished. Use
http_request: verify_ssl: false for self-signed HA. Skip reloading the same URL to
cut flicker. Cover URL comes from the player entity's entity_picture.
-
Fonts, icons and game sprites are fetched at compile time (Google Fonts +
MaterialDesign TTF, plus the PNGs under assets/sprites/cool-cars/ and
space-wars/ from the GitHub repo), so the build host needs internet. They are
baked into the firmware, so at runtime nothing depends on any server.
-
Battery is a heuristic. ADC on GPIO6 (×2), 64-sample oversampling + moving
average to fight ETA6003 ripple (no cap on BAT_ADC). Voltage→% is a calibrated
lookup (vt[]/st[]) - start ~4.10 V, brownout ~3.20 V - not finished, tune per
unit. No STAT pin, so charging detection is "voltage rising or ≥4.13 V".
-
Voice timers: use voice_assistant: on_timer_started/updated/finished/ cancelled. There is no on_timer_tick - do smooth local countdown in an
interval and let HA fire finished.
-
Wake word (micro_wake_word) only runs while connected to HA. Start/stop it in
on_client_connected/on_client_disconnected and around the VA lifecycle.
-
Debugging: API has no encryption here → stream logs with
python scripts/esplog.py <seconds> (set the device host in the script). Don't reflash
blind - check logs / HA state first.
-
Timezone resets to UTC at runtime. The homeassistant time platform syncs the epoch
but not the zone, so set timezone: explicitly (IANA, e.g. Europe/Warsaw). Worse,
ESPHome can reset the device TZ back to UTC mid-run (on a menu restart, or when a log/API
client reconnects), so the clock reads UTC until the next sync. Self-heal: capture the
resolved POSIX zone at boot via getenv("TZ") into a global, then re-apply it every second
with setenv("TZ", g.c_str(), 1); tzset();. Note RealTimeClock has set_timezone() but
no get_timezone(), and passing the IANA name at runtime won't work (newlib needs the
POSIX string) - which is why you read it back from the env.
-
OTA rollback reverts to the OLD firmware after a quick restart. On esp-idf the bootloader
keeps a freshly-OTA'd image only once the boot is marked "good" (safe_mode, default ~1 min).
Restarting (e.g. from the on-screen menu) before that rolls back to the previous build - which
looks exactly like "my changes didn't stick". Fix: add a safe_mode: block and call
safe_mode.mark_successful once the device is clearly up (this repo does it when the boot
splash ends), and/or lower boot_is_good_after.
-
${substitution} inside a flow-mapping { } breaks the YAML parse. The } that closes
${...} is read as the end of the flow map, e.g. image: { id: x, src: avatar${n} } fails
with "expected ',' or '}'". Use block style for any line whose value contains a ${...}.
-
Bluetooth is BLE-only and competes hard with voice/LVGL. The S3 has Bluetooth LE 5.0
and no Classic - so no Bluetooth audio (A2DP needs Classic; playback stays on the I²S
DAC). What ESPHome can do here is BLE: bluetooth_proxy (relay BLE devices to Home
Assistant) or esp32_ble_tracker (beacons / BLE sensors), optionally esp32_improv (BLE
Wi-Fi provisioning). It is not enabled in this repo, and it is not free: the BLE stack +
Wi-Fi/BLE coexistence share the single 2.4 GHz radio and CPU with micro_wake_word, the
voice pipeline and the QSPI/LVGL redraws, and the BLE stack wants internal RAM that is
already squeezed (LVGL draw buffer at 24% internal). Expect audio/display hitches, higher
heap pressure and possible OOM/brownouts. If you add it: prefer passive scanning, keep
active connections to a minimum (bluetooth_proxy raises the BLE connection slots), test heap
headroom (logger + free-heap sensor), and consider dropping heavy screens. Treat it as
untested on this exact build - validate on-device before relying on it.
When editing, change UI strings/labels only (not IDs or pin numbers), keep the
performance settings above, and validate the YAML before flashing.