Nextion Editor — Human Guide¶
The display UI is a Nextion Editor project (nextion/nspanel_haui.HMI) and
the compiled firmware is the .tft files. Most of this repository's tooling
cannot produce those files for you: layout changes, new pages, new widgets and
the portrait variant for the US model are human work in the Nextion Editor
application. This document is the instruction sheet for that work and for
keeping the HMI in sync with the Python side of the integration.
It deliberately walks through everything a human has to know. For the integration-side view (which commands the hub sends and how rendering works) see Nextion Component; for the hardware models see Display Models.
1. When you need this document¶
- Moving / resizing / recolouring an existing widget (quick fix).
- Adding a widget or a whole new page to the UI.
- Adding fonts or images (icons, backgrounds, spinners).
- Maintaining both TFT layouts: the standard landscape
(
nspanel_haui.tft, EU + US mounted landscape) and the rotated portrait (nspanel_haui_portrait.tft, US mounted portrait). - Releasing a new version (both assets must ship together).
2. Prerequisites & tools¶
| Tool | Purpose |
|---|---|
| Nextion Editor (official, free; Windows) | Edit nspanel_haui.HMI, compile to .tft |
Nextion2Text — local copy: scripts/nextion2text/Nextion2Text.py (see scripts/nextion2text/README.md) |
Dump the HMI to readable text to verify IDs/names/coordinates |
| The repo's Python test stack | Verify the Python component registries still match |
The HMI is a binary project file — open it in the Nextion Editor, never edit it by hand. Choose the project variant matching the NSPanel's display (480 × 320 landscape).
Verification command for a landscape HMI:
python scripts/nextion2text/Nextion2Text.py -i nextion/nspanel_haui.HMI \
-o build/nextion-out/landscape -d -s -p visual
Use the matching -i for the portrait HMI (see
§7 Creating the US portrait TFT) and a
different -o folder (e.g. build/nextion-out/portrait). build/ is
git-ignored — safe to regenerate.
3. The sync contract between HMI and Python¶
The hub renders the display by name (tHeader.txt="...") and maps touches by
id + object name. Every widget the Python side touches must exist in the
HMI with matching coordinates of truth:
| HMI side | Python side |
|---|---|
| Widget object name on a page | Component(id, "name") entry in that page's COMPONENTS (ComponentRegistry) in custom_components/nspanel_haui/haui/page/<page>.py |
| Widget id (page-local, 1-based) | the Component.id int — must match exactly |
| Page id in the page properties | the page class DESCRIPTOR.page_id, mapped via PAGE_MAPPING (haui/mapping/panel.py) |
| Page name | the page class DESCRIPTOR.page_name and PAGE_MAPPING |
Example: if the HMI page clock has widget tTime with id 3, Python has
t_time=Component(3, "tTime") and calls self.set_component_text(self.COMPONENTS.t_time, ...).
Hard rules¶
- Every page must send
sendmein itsPreinitializeevent. The hub waits for theesphome.pageevent (the Nextionsendmepostulate) before rendering; a page without it will never be rendered. - Prepare the page in
Preinitialize(hide unused widgets, set picture backgrounds, clear text). The hub cannot touch the display before the page is shown. - Enable
Send Component IDon any touch event the hub needs to react to (buttons, sliders, keypad keys). Without it the hub receives no component event for the widget. - Component ids are page-local and change when you reorder widgets.
After reordering, re-dump the page and fix every affected
Component(id, …)entry in Python. - Keep logic in the Hub App, not in HMI events. Layout/visuals live in the
HMI; behaviour (state reads, commands, navigation) lives in
haui/page/*.py. HMI event code beyondsendmeshould be the exception. - Respect the visible area. The landscape panel is 480 × 320 px but the bezel hides the rightmost 30 px — keep interactive/important content left of x = 450 (see Nextion Component).
- Version text lives in the HMI too. If a page (e.g. System/About) shows
a version, keep it in sync with
custom_components/nspanel_haui/manifest.jsonandesphome/nspanel_haui/version.yamlon release.
4. Typical workflow: change an existing widget¶
The most common human task — reposition, resize, recolour, or retitle a widget. No Python changes are needed unless you also change the object name or id.
- Open
nextion/nspanel_haui.HMIin the Nextion Editor. - Open the page, select the widget (click on the canvas, or find it in the page's component list).
- Edit its properties (x, y, w, h, font id, colours,
.txtdefault, picture). - If the hub writes the value at runtime (text, font size, colors, pictures), the HMI values are only the initial look — the hub overrides them on every render. Don't be surprised that a changed default is overwritten.
- If the hub sets colors from the HA palette, those come from
haui/mapping/color.pyCOLORS, not from the HMI. - Geometry-only changes (position, size, slider cursor) are better kept in
scripts/gen_portrait/landscape_layout.pyand applied withgen_portrait.py --spec landscape_layout(project closed first, see §7): the spec documents the layout rules and the same change then goes intoportrait_layout.py. - Compile (
Build) to regenerate the TFT you're testing. - Re-dump with Nextion2Text and confirm the widget's id/name are unchanged (or update Python if they are).
- Test on the device: render the page, touch the widget, watch the hub log.
5. Add a widget or a whole page¶
Adding one widget to an existing page¶
- In the editor, add the widget on the page (e.g. a
textfor a new sensor readout, abuttonfor a new action, agauge/sliderfor a value). - Note its object name and id.
- In
haui/page/<page>.py, extend the registry if the page subclasses a base that already declares some entries:
class MyPage(SomeBasePage):
COMPONENTS = SomeBasePage.COMPONENTS.merge(
t_new_readout=Component(9, "tNewReadout"),
)
If the page's registry is standalone (like CommonNotifyPage), add the
Component(...) line to its ComponentRegistry(...).
4. Use the component: self.set_component_text(self.COMPONENTS.t_new_readout, …)
or self.on_release(self.COMPONENTS.btn_x, self.callback_x) with a handler
def callback_x(self, event, component): ….
5. If the widget shows user-facing text or a title attribute, run the
translation extraction (python scripts/translations/extract_translations.py)
after adding the t()/self.translate() call.
6. Re-dump + run pytest tests/, ruff, mypy (see §8).
Adding a new page¶
- In the editor: add a page, give it a unique page id and a page name
(e.g.
dehumidifier). - In its
Preinitialize, sendsendmeand prepare the page state. - Add all widgets; enable
Send Component IDon the interactive ones. - In Python, create
haui/page/dehumidifier.pywith aHAUIPagesubclass, aCOMPONENTSregistry, and aDESCRIPTOR(uniquetype_key,page_id/page_namematching the editor,label/descriptionvia_()). - Register it in
haui/mapping/panel.py(_page_classeslist) — the registration logic itself is closed for modification. - Add a preview renderer (
scripts/gen_preview/gen_preview.py --type <key>), document the panel indocs/panels/panel_<key>.md, and run the checks.
6. Fonts and images¶
- Icons are font glyphs. The MDI icon set is baked into custom Nextion
fonts in
nextion/fonts/(Roboto-Regular <size> (UTF_8).zi,Roboto-Light <size> (UTF_8).zi), generated by the font scripts inscripts/gen_fonts/(seescripts/gen_fonts/README.md). The hub maps an icon name to its code point viahaui/mapping/icons.pyand writes the single glyph character into a text widget. You cannot add a new icon to the display without (a) generating an updated font.zi, (b) importing it into the HMI in the Nextion Editor, and (c) adding the name→glyph mapping tohaui/mapping/icons.py. - Font sizes must exist per widget. A text widget references one of the
embedded fonts by id. When you change a widget's font in the editor, use a
size that actually exists in the project's font list, and make sure the hub's
font=override (e.g. the notify page's auto-sizing) picks a size the device has. - Pictures/backgrounds live in
nextion/images/(bkg/,grid_landscape.png,grid_portrait.png, …). Backgrounds are imported as picture resources; the hub sets.pic/.piccfromhaui/mapping/. Some pages (About, Clock, Clock Two, Weather) are entirely picture-background based — their look is the PNG, not widgets. The background tiles are generated: put the source picture innextion/src_images/bkg/, named after its key inhaui/mapping/background.py(systemis themodernpicture), and runpython scripts/gen_backgrounds/gen_backgrounds.py. It writes the dimmed 480 × 320 and 320 × 480 tiles tonextion/images/bkg/landscape/andnextion/images/bkg/portrait/. The twogrid_*.pngfiles are design-aid page backgrounds for the editor (margins, column and row guides, placeholder sizes, font samples, theme colors as RGB565); regenerate them withpython scripts/gen_grid/gen_grid.py. The system page's spinner is two sprite sheets per orientation (spinner_<orientation>_1.png/_2.png, pictures 10 and 11) whose frames are drawn on thesystemtile cropped at the spinner's position;python scripts/gen_spinner/gen_spinner.pyre-renders them, so run it after changing that tile or movinghSpinner. - RGB565 images for ad-hoc assets can be converted with
scripts/gen_rgb565_image/scripts/rgb_conv.
7. Creating the US portrait TFT¶
The US model mounted portrait needs a rotated layout: the same pages,
object names and ids as the landscape project, different geometry.
scripts/gen_portrait/ keeps that geometry reproducible —
portrait_layout.py holds the 320 × 480 position of every widget and
gen_portrait.py writes it into the portrait HMI. The script also re-computes
the per-page content stamp the editor validates (a hand-edited page block
otherwise fails with "Version mismatch" / "Loading Page Failed"). The editor is
still needed to set the display direction, import pictures and compile; ship
the result alongside the landscape TFT on every release.
7.1 First creation (or a fresh re-rotation)¶
- In the Nextion Editor, open
nextion/nspanel_haui.HMI. - Save a copy as its own project before rotating anything:
File → Save As…→nextion/nspanel_haui_portrait.HMI. Commit this file — it is the versioned source of the portrait asset (same as the landscape HMI is fornspanel_haui.tft). - In the editor's device settings, switch the display orientation of this
copy to portrait (320 × 480), then apply the geometry from the repo —
gen_portrait.pypatches widget geometry only, not the orientation:
python scripts/gen_portrait/gen_portrait.py --check # spec vs. HMI widgets
python scripts/gen_portrait/gen_portrait.py # patch the portrait HMI in place
The check fails when a widget exists on only one side, so a widget added
to the landscape project needs its PORTRAIT_LAYOUT entry first. It now
also verifies attribute values (alignment, font, …) against the spec, so a
change to PORTRAIT_ATTRS cannot pass while the committed HMI still has
the old value. Only x/y/w/h (plus the page size) and the spec's attribute
overrides change; names, ids and sendme are untouched, and every widget's
Send Component ID is copied from the landscape HMI (the spec's
SIBLING_HMI) — toggle touch settings in the landscape project, then
re-run the script. A PORTRAIT_ATTRS change must also be followed by a
TFT recompile (step 4 below): the HMI change alone does not reach devices.
The layout rules the spec follows:
- The header bar (tHeader/tTitle) spans the full top 50 px; the four
function buttons move to a bottom bar at y 430..480, so the content area
is y 50..430 with 12 px side margins.
- Grids re-flow (grid 3 × 2 → 2 × 3, select 3 × 4 → 2 × 6), rows stack
their controls under the name line, sliders grow taller, button rows
keep their order.
- Re-open the project and check every page in the editor; text that no
longer fits its (narrower) widget needs the next font size down.
- Icons/backgrounds: either rotate the pic resources in the editor or
re-import pre-rotated PNGs from nextion/images/ (the background tiles
in nextion/images/bkg/portrait/ are already 320 × 480).
- Keep every widget's object name identical to the landscape page.
Only the geometry may differ.
- Keep sendme and Send Component ID settings identical.
- Ids may change if reordering happened; if they do, update the Python
COMPONENTS registries accordingly — the hub must address the portrait
page with the same names/ids it addresses the landscape one (the page id
and name are the same; only the TFT asset differs).
4. Compile (Build) → nextion/nspanel_haui_portrait.tft.
5. Re-dump the portrait HMI and diff the component names against the Python
registries.
6. Mirror the portrait widget geometry into the editor's panel previews, or
a portrait device keeps showing landscape placement. The previews follow
the device's display_model: frontend/previews/registry.js stamps
portrait on every preview result, frontend/panel-card.js then draws
the 320 × 480 frame with the function buttons in a bottom bar, and the
orientation-specific rules live under .pg-card-preview--portrait in
frontend/styles/preview/*.js (a renderer can also branch on
isPortraitDevice(host) from frontend/previews/utils.js, as the word
clock does). Re-check each preview with a portrait device selected.
7.2 Keeping both variants in sync¶
- The two HMIs are siblings — treat the portrait one as a view of the landscape one. When you add a widget/page in the landscape HMI, repeat the "add widget/page" steps in the portrait HMI (same name, id, sendme, touch settings; different geometry) and keep the page lists identical.
- There is one definition of components. The hub's Python
COMPONENTSregistries address both TFTs with the same names and ids, so any change applied to the landscape HMI — widgets, ids, attributes, event code, and geometry or slider tweaks inscripts/gen_portrait/landscape_layout.py— must be applied to the portrait HMI as well (and vice versa), with its geometry expressed inportrait_layout.py. Be extra careful here: a widget missing on one side only fails on the device.gen_portrait.py --checkfor both specs andverify_nextion/verify_nextion.py --require-portraitare the guard rails. - Both
.tftfiles must be attached to every GitHub release. The hub's update controller downloadsnspanel_haui.tftfordisplay_model: eu(the default landscape layout) andnspanel_haui_portrait.tftfordisplay_model: us(see Display Models).
7.3 Portrait-specific caveats¶
- Gesture edge detection is model-aware. The ESPHome scripts
(
esphome/nspanel_haui/scripts_interaction.yaml) read the Display Model number the hub syncs on every connect:0uses the landscape 450×320 edge dimensions,1uses the portrait 320×480 ones. Edge swipes therefore behave the same in both layouts — no HMI change needed. - Touch coordinates on the portrait device are not the same as landscape; the hub maps touches by page/component id + name, so as long as ids/names match, on-screen controls work — but verify every button manually on real hardware.
- Keep the release checklist honest: a release that ships only the landscape
TFT will leave
display_model: usdevices on an old TFT (the update flow just finds no matching asset).
8. Verification checklist (after any HMI change)¶
- Re-dump both HMIs:
python scripts/nextion2text/Nextion2Text.py -i nextion/nspanel_haui.HMI -o build/nextion-out/landscape -d -s -p visual
python scripts/nextion2text/Nextion2Text.py -i nextion/nspanel_haui_portrait.HMI -o build/nextion-out/portrait -d -s -p visual
- Cross-check widget names/ids against
haui/page/*.pyCOMPONENTSregistries page by page. - Full checks:
pytest tests/,ruff check custom_components/ scripts/,mypy, and if you touchedfrontend/,node --test 'tests/frontend/*.test.mjs'. - If you added user-facing strings:
python scripts/translations/extract_translations.py. - Automatic release gate:
python scripts/verify_nextion/verify_nextion.py --require-portraitfails the release until (a)clock.tTimehas Send Component ID enabled, (b)system.tftVersionmatchesmanifest.json, and (c) the portrait HMI/TFT exist. Run it locally after every HMI edit; the release workflow runs it too. For a release, set the version first withpython scripts/bump_version.py X.Y.Z— it edits the HMItftVersiontext directly (same-length versions only) so you do not have to type it in the Nextion Editor. A length-changing version still needs the Editor. - On-device test per model: page renders completely, all widgets hit-test
correctly, gestures behave (see §7.3 for portrait), auto-update installs the
right TFT for
display_model: us. - Release: attach both TFTs (+ both HMI sources) and keep
manifest.json/version.yaml/ HMI version text in lockstep.