An ESPHome config that turns a Waveshare ESP32-S3-Knob-Touch-LCD-1.8 (a.k.a. Guition JC3636K518C-I-YR1) into a controller and shot display for a La Marzocco Micra, with shot data from a Mahlkönig E64 WS + Sync scale.
Everything runs through Home Assistant. Nothing depends on a cloud service at runtime — fonts and the logo are baked into the firmware at compile time.
The shot timer starts on its own. The knob watches the machine — pull a shot and it jumps straight to a live timer with a filling ring. No tap, no old phone propped against the group head.
When the Sync data lands, the result card shows the brew ratio, dose to yield, shot time against the recipe target, the machine's own verdict, and a two-step rating you dial in on the knob:
Turning the knob walks a carousel of five cards. Three more screens appear on their own when the machine does something.
| Screen | When | Shows |
|---|---|---|
| Main | card 1, idle | La Marzocco mark, STANDBY / HEATING / READY, last shot ratio + time + score, power and menu buttons |
| Shot result | card 2, and automatically when the Sync bundle lands | brew ratio, dose → yield, shot time vs the recipe target, the machine's own verdict, grind correction, and your rating |
| Grinder | card 3 | dose, grind setting, grind time, which beans are in the hopper, days off roast and brew window |
| Care | card 4 | time since the last backflush, total shots, water tank |
| Boilers | card 5 | coffee target temperature, and both boilers' heating / ready state |
| Control | tap the gear | steam boiler · steam level · coffee temp · target ratio · beans · backflush · back — knob to move, tap to act, hold to leave |
| Shot timer | brewing_active turns on |
live seconds counter and a filling arc round the bezel |
| Backflush | a cycle starts | paddle prompt, then elapsed time and a filling arc |
Hold on the shot-result card to rate it. Two steps, turn to pick, tap to advance: taste (sour ↔ bitter, the only axis that means something mechanical) and then score 1–5. Both are written to Home Assistant on the last tap only, so abandoning halfway records nothing.
Committing also calls a Home Assistant script that appends one row to a to-do list acting as a shot log. The knob pushes the measured dose, yield and grind setting across first, because those live in Home Assistant helpers that are otherwise typed by hand.
Earlier versions inferred one from steam boiler recovering while the coffee boiler is not. The machine publishes no steaming signal, and every version of that inference produced false positives — a cold-start warm-up, the steam boiler being switched on by itself, a routine top-up reheat. It was removed rather than narrowed again. The boilers card shows the measured steam-boiler state instead.
This is the K518 / K-series board — the one with the rotating knob around the screen. Its pinout differs from both the W518 and the K718. Do not reuse pins across them.
Display QSPI (ST77916): CLK 13 CS 14 D0-D3 15/16/17/18 RST 21
Touch CST816 (I²C): SDA 11 SCL 12 INT 9 RST 10 (chip answers at 0x15)
Haptics DRV2605: same I²C bus, 0x5A, EN strapped to 3V3, TRIG to GND
Knob EC1: A GPIO8 B GPIO7
Backlight: GPIO47 (PWM)
Pin values are taken verbatim from KrX3D/WaveShare-Knob-Esp32S3 and cross-checked against the board schematic in that repo. The unmodified upstream files are kept in reference/ so you can diff them.
16 MB flash means the first flash must be over USB. A partition table change can't be applied over the air. Flash once via web.esphome.io with a USB-A → USB-C cable, then everything after that is OTA.
On this board the cable orientation selects which of the two chips you talk to. With a C→C cable it's a coin flip which one you get.
Collected so nobody else has to rediscover them.
The board has two rotary encoders, SW1 and SW2 (both SSCM110100), wired one per MCU:
EC1_A→GPIO8,EC1_B→GPIO7 — on the ESP32-S3EC2_A→IO19,EC2_B→IO22 — on the second, plain ESP32
So the S3 has a complete A/B pair of its own and reads both directions unaided. You never need to flash the second MCU. Note it is not a quadrature encoder in the PCNT sense — one clean pulse per detent, direction encoded by which channel fires — so ESPHome's stock rotary_encoder reads zero. This config uses rotary_encoder_custom from KrX3D.
The knob has no push contact. SW2 is a 4-pin part: two commons to ground, A and B out. There is no press to detect.
The bus scan finds 0x5A and writes work fine — but reads don't. RAR/esphome-drv2605 reads the status register first thing in setup(), so it bails out before setting LRA mode and selecting library 6, then marks itself FAILED. Effects still fire (because writes work), so you get a buzz — just the wrong one, running on ERM defaults with an LRA motor.
This config drops the component and writes the six registers directly over I²C instead:
0x01 MODE = 0x00 out of standby, internal trigger
0x1A FEEDBACK = 0x80 LRA
0x03 LIBRARY = 0x06 LRA library
0x04/0x05/0x0C waveform, terminator, GO
On ESPHome 2026.7+, a single-expression lambda gets inlined, skipping the std::string conversion, and .c_str() is then called on the result. Anything that returns std::string from a one-liner fails to compile. Use a multi-statement lambda:
text: !lambda |-
static char b[28];
snprintf(b, sizeof(b), "1 : %.1f", id(last_ratio));
return b;The add-on's config directory only accepts text files, so you can't upload a PNG. But ESPHome detects SVG by sniffing for <svg in the first bytes and rasterises it with cairosvg at compile time — so an SVG works, and an SVG can carry a raster inline as a base64 data: URI if you don't have a vector source.
LVGL rejects a rotated display and rotates touch input itself via rotate_coordinates(). Put rotation: 180 on the lvgl: block.
When you adopt an ESPHome device, allow_service_calls defaults to off. Every homeassistant.action silently does nothing, with no error anywhere. Settings → Devices & Services → ESPHome → Configure.
Calling switch.toggle on the machine produces this, and it does it from the HA UI too — the knob isn't involved:
17:17:04.733 on
17:17:04.737 off ← 4 ms later
17:17:06.762 on ← 2 s later, settles
The integration sets the state optimistically, its Bluetooth call to the machine fails (components/lamarzocco/switch.py, out of BLE connection slots), it rolls back, then the cloud confirms. The config masks this by holding its own intent for 6 seconds after a press. The real fix is a Bluetooth proxy near the machine.
Every interval between on and off is an exact whole number of seconds (15:59:43.332 → 15:59:47.333). The on-screen timer therefore starts up to a second after you move the lever. The authoritative figure is shot_time from the Sync scale, which is what the result card shows.
All the shot sensors change within ~5 ms of each other and yield_weight records exactly one state change per shot. Live grams during extraction are not possible. The config triggers on the grind_event_uuid attribute — unique per grind, so repeat shots with identical numbers still fire exactly once — and waits 800 ms for the whole bundle to land before reading.
grind_setting is disc distance in microns and Grind-by-Sync rewrites it after every shot from the scale's duration and yield, so the interesting number is the delta. grind_setting_target is the from-scratch starting point, not a target being chased.
- Copy
coffee-knob.yaml,lamarzocco.svgand a filled-insecrets.yamlinto your ESPHome config directory. - Edit the
substitutions:block to match your entity IDs. - Build, download the factory binary, flash once over USB via web.esphome.io.
- Adopt in Home Assistant and turn on
allow_service_calls.
Everything after the first flash is over the air.
- KrX3D/WaveShare-Knob-Esp32S3 — the bring-up this is built on, the
rotary_encoder_customcomponent, and the board schematic - nkinnan/Waveshare-ESP32-S3-Knob-Touch-LCD-1.8… — hardware documentation, including the cable-orientation quirk
- MichalZaniewicz/esphome-guition-jc3636k718c-va — the K718 sibling; different pinout, but its notes on LVGL buffer sizing and the non-quadrature knob saved real time
- RAR/esphome-drv2605 — haptics component
- ESPHome discussion #3253 — the community thread on this board
The La Marzocco name and mark are trademarks of La Marzocco S.r.l., used here only to identify the machine this controls. Not affiliated with or endorsed by La Marzocco or Mahlkönig.
MIT — see LICENSE.

