<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Home-Assistant on Andy Little</title><link>https://andylittle.net/tags/home-assistant/</link><description>Recent content in Home-Assistant on Andy Little</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 18 Sep 2026 11:30:00 -0700</lastBuildDate><atom:link href="https://andylittle.net/tags/home-assistant/index.xml" rel="self" type="application/rss+xml"/><item><title>A 10-Inch ESP32-P4 Touch Panel as a Home Assistant Dashboard</title><link>https://andylittle.net/blog/2026/esp32-p4-touch-panel-home-assistant/</link><pubDate>Fri, 18 Sep 2026 11:30:00 -0700</pubDate><guid>https://andylittle.net/blog/2026/esp32-p4-touch-panel-home-assistant/</guid><description>A $67 Guition JC8012P4A1C turned into a Home Assistant wall panel with ESPHome and LVGL. The newest batch needed a patched display driver to stop a boot loop, and the touchscreen stayed dead through three drivers and two firmware tables until I unplugged it from the dock.</description><content:encoded><![CDATA[<p>I wanted a wall panel for Home Assistant: a few lights, a few switches, and the
thermostat, on a screen big enough to hit without looking. The board I landed
on is a Guition <strong>JC8012P4A1C</strong>, sold on AliExpress for about $67. It&rsquo;s a
10.1-inch, 800×1280 capacitive touchscreen in a case, driven by an
<strong>ESP32-P4</strong> with an <strong>ESP32-C6</strong> alongside for WiFi.</p>
<p>There&rsquo;s no Linux on it, so no kiosk browser pointed at a Lovelace dashboard.
The P4 is a microcontroller. That makes it an <a href="https://esphome.io/">ESPHome</a>
job, with <a href="https://lvgl.io/">LVGL</a> drawing the UI and the native Home
Assistant API carrying state both ways.</p>
<p>The config is on GitHub:
<a href="https://github.com/andylittle/lcd-touchscreen">andylittle/lcd-touchscreen</a>.</p>
<h2 id="which-board-do-you-actually-have">Which board do you actually have</h2>
<figure class="align-center ">
    <img loading="lazy" src="label.jpg#center"
         alt="Rear label: SKU 10153001-V3 (2635), model JC8012P4A1C_I_W_Y, resolution 800*1280" width="560"/> 
</figure>

<p>The rear label matters more than it should. Guition has shipped at least three
variants under the same model name:</p>
<ul>
<li><strong>Batch below 2624:</strong> the original display panel. ESPHome&rsquo;s <code>mipi_dsi</code> model
is <code>JC8012P4A1</code>.</li>
<li><strong>Batch 2624 and later:</strong> a new display panel with a different init
sequence. The model is <code>JC8012P4A1-V2</code>.</li>
<li><strong>The newest boards</strong> also moved to production ESP32-P4 silicon, revision
v3.x, which needs different firmware than the earlier engineering samples.</li>
</ul>
<p>Mine says <code>10153001-V3 (2635)</code>. <code>esptool</code> reported <code>ESP32-P4 (revision v3.2)</code>.
So it&rsquo;s the V2 panel on v3 silicon, which in ESPHome means
<code>engineering_sample: false</code>. Most community configs I found still had <code>true</code>,
from the older boards.</p>
<figure class="align-center ">
    <img loading="lazy" src="board.jpg#center"
         alt="JC8012P4A1 board with the back cover removed, a USB cable plugged into the left USB-C port" width="400"/> 
</figure>

<p>With the back off: three USB-C ports along the bottom. The left one, marked
<strong>USB_UART</strong>, goes through a CH340 to the P4&rsquo;s UART0. That&rsquo;s the one to flash
and read logs through. It shows up as <code>/dev/ttyUSB0</code>, not <code>ttyACM0</code>, and
ESPHome needs <code>logger: hardware_uart: UART0</code> or you only see the ROM boot
text. The C6 WiFi module sits near the top. The C6 talks to the P4 over SDIO:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">esp32_hosted</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">variant</span><span class="p">:</span><span class="w"> </span><span class="l">esp32c6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">reset_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO54</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cmd_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO19</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">clk_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO18</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">d0_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO14</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">d1_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO15</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">d2_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO16</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">d3_pin</span><span class="p">:</span><span class="w"> </span><span class="l">GPIO17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">active_high</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h2 id="esphome-needs-python-312">ESPHome needs Python 3.12</h2>
<p>ESPHome 2026.7 and later require Python 3.12. Debian 12 ships 3.11, so
<code>pip install esphome</code> found nothing newer than 2026.6. PEP 668 blocks
<code>pip install --user</code> on Debian too. What worked was bootstrapping
<a href="https://github.com/astral-sh/uv">uv</a> in a throwaway venv and letting it fetch
an interpreter:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m venv /tmp/uvboot <span class="o">&amp;&amp;</span> /tmp/uvboot/bin/pip install uv
</span></span><span class="line"><span class="cl">/tmp/uvboot/bin/uv venv --python 3.12 .venv
</span></span><span class="line"><span class="cl">/tmp/uvboot/bin/uv pip install --python .venv/bin/python <span class="s2">&#34;esphome&gt;=2026.9.0,&lt;2026.10&#34;</span>
</span></span></code></pre></div><p>A venv hard-codes its own absolute path, so moving the project folder later
broke it with <code>cannot execute: required file not found</code>. Recreating it fixed
that. ESPHome&rsquo;s build cache in <code>.esphome/</code> also remembered the old path, and
the upload went looking for a <code>firmware.factory.bin</code> in a directory that no
longer existed. Deleting <code>.esphome/</code> and rebuilding fixed that one.</p>
<h2 id="boot-loop-number-one-the-display-clock">Boot loop number one: the display clock</h2>
<p>The first flash booted, initialized touch, and died:</p>
<pre tabindex="0"><code>[C][component:164]: Setup touch
abort() was called at PC 0x48046255 on core 1
Rebooting...
</code></pre><p><code>addr2line</code> against the ELF put it inside ESP-IDF, called from ESPHome&rsquo;s
display setup:</p>
<pre tabindex="0"><code>_mipi_dsi_ll_set_phy_pllref_clock_source at hal/esp32p4/include/hal/mipi_dsi_ll.h:218
esp_lcd_new_dsi_bus at esp_lcd/dsi/esp_lcd_mipi_dsi_bus.c:61
esphome::mipi_dsi::MipiDsi::setup() at mipi_dsi.cpp:40
</code></pre><p>That function is a <code>switch</code> over clock sources that ends in <code>abort()</code>. On P4
v3 silicon, IDF compiles a different version of it with a different set of
valid sources. ESPHome 2026.9.0 passes <code>MIPI_DSI_PHY_CLK_SRC_DEFAULT</code>, which on
IDF 5.5 is defined as the <em>legacy</em> source from the pre-v3 chips. The rev 3
<code>switch</code> has no case for it, so the default branch aborts.</p>
<p>ESPHome had already fixed this upstream in
<a href="https://github.com/esphome/esphome/pull/18984">#18984</a> on September 5, by
passing 0 and letting IDF pick. It just missed the 2026.9.0 release. The
workaround is to vendor the component with that one line changed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="n">esp_lcd_dsi_bus_config_t</span> <span class="n">bus_config</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">bus_id</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">num_data_lanes</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">lanes_</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 0 lets ESP-IDF pick the right default for the chip revision.
</span></span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">phy_clk_src</span> <span class="o">=</span> <span class="p">(</span><span class="n">mipi_dsi_phy_clock_source_t</span><span class="p">)</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">lane_bit_rate_mbps</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">lane_bit_rate_</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">external_components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">components</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">components</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">mipi_dsi]</span><span class="w">
</span></span></span></code></pre></div><p>Once ESPHome 2026.10 ships, the folder can go.</p>
<h2 id="the-dashboard">The dashboard</h2>
<p>The layout is one landscape page. Four light cards: tap to toggle, drag the
slider for brightness. Four switch cards. A full-width thermostat card with the
current temperature, the setpoint with +/− buttons, and heat, cool, auto and
off modes.</p>
<p>Every Home Assistant entity is a substitution at the top of <code>panel.yaml</code>, so
pointing a card at a different light is a one-line change:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">substitutions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">light_1</span><span class="p">:</span><span class="w"> </span><span class="l">light.living_room_lamp_one</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">light_1_name</span><span class="p">:</span><span class="w"> </span><span class="l">LR Lamp One</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">climate_main</span><span class="p">:</span><span class="w"> </span><span class="l">climate.downstairs</span><span class="w">
</span></span></span></code></pre></div><p>State flows both ways. A <code>homeassistant</code> binary sensor mirrors each entity into
the panel and updates the card&rsquo;s checked state, so the card follows changes made
anywhere else:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">binary_sensor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">platform</span><span class="p">:</span><span class="w"> </span><span class="l">homeassistant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">st_light_1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">entity_id</span><span class="p">:</span><span class="w"> </span><span class="l">${light_1}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">on_state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">lvgl.widget.update</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">btn_light_1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">checked</span><span class="p">:</span><span class="w"> </span>!<span class="l">lambda return x;</span><span class="w">
</span></span></span></code></pre></div><p>A tap calls the action back:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">on_click</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">homeassistant.action</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">light.toggle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">entity_id</span><span class="p">:</span><span class="w"> </span><span class="l">${light_1}</span><span class="w">
</span></span></span></code></pre></div><p>That last part does nothing until you allow it. In Home Assistant, open the
ESPHome integration entry for the device and turn on <strong>Allow the device to
perform Home Assistant actions</strong>. It&rsquo;s off by default. The panel happily shows
state without it and silently ignores every tap.</p>
<h2 id="the-touchscreen-that-wouldnt-start">The touchscreen that wouldn&rsquo;t start</h2>
<p>With the display fixed, the screen showed the dashboard, and Home Assistant
changes showed up on it within a second. Touching it did nothing.</p>
<p>The touch controller is a Silead <strong>GSL3680</strong>, and it&rsquo;s an odd part. It has no
flash of its own. On every boot the host uploads about 4,500 register writes
of firmware to it over I2C, starts it, and then checks a marker: register
<code>0xB0</code> should read <code>5A 5A 5A 5A</code> once the firmware is running. Mine read zeros:</p>
<pre tabindex="0"><code>[D][touchscreen.gsl3680]: Read configuration #2: 12, 34, 56, 0
[D][touchscreen.gsl3680]: Load firmware complete
[D][touchscreen.gsl3680]: Read RAM: 0, 0, 0, 0
[E][touchscreen.gsl3680]: Unexpected byte in read_ram: got 0x0, expected 0x5a
</code></pre><p>The chip was answering. It echoed a test write back correctly and accepted the
whole upload without an I2C error. The firmware just never ran.</p>
<p>I went through everything I could find that differed from a working setup:</p>
<ul>
<li>ESPHome&rsquo;s built-in <code>gsl3670</code> driver, which loads a firmware table meant for a
Seeed board. No touches.</li>
<li>The community <code>gsl3680</code> driver from the original ESPHome thread for this
board, and espcontrol&rsquo;s fork of it. Same zeros.</li>
<li>The firmware table from Guition&rsquo;s own demo code, which differs from the one
in the community driver. Same.</li>
<li>The stock init order: a second reset and start after the upload, polling the
marker ten times, touch initialized after the display. Same.</li>
<li>100 kHz I2C, and the backlight held off during touch init. Same.</li>
<li><a href="https://github.com/jtenniswood/espcontrol">espcontrol</a>&rsquo;s prebuilt V3
firmware, flashed as-is. Same failure, in their build too.</li>
</ul>
<p>The breakthrough was a boot log from the stock Guition firmware, which another
owner had attached to
<a href="https://github.com/jtenniswood/espcontrol/issues/1939">an espcontrol issue</a>
about these exact batch-2635 boards. Same chip revision, same sequence, and
their chip came up:</p>
<pre tabindex="0"><code>I (3646) gsl3680: load fw success
I (3756) gsl3680: gsl3680 startup_chip failed read 0xb0 = 5a,5a,5a,5a
</code></pre><p>(The message says &ldquo;failed&rdquo; either way. The values are what count.) Another
owner in the same thread had touch working with the espcontrol build that
failed for me. Same firmware, same batch, different result. That pointed away
from software.</p>
<p>The panel was plugged into a USB-C port on my laptop dock. I moved the cable to
a port on the laptop itself:</p>
<pre tabindex="0"><code>[D][touchscreen.gsl3680]: Read RAM (attempt 1/10): 5a, 5a, 5a, 5a
[I][touchscreen.gsl3680]: GSL3680 firmware running
</code></pre><p>First try. The 10-inch backlight, the P4, and the C6&rsquo;s radio were drawing more
than the dock port supplied, and the touch controller was the one thing that
failed. The display, WiFi and the Home Assistant connection all looked
perfectly healthy the whole time, which is what sent me looking at software.
I kept the backlight-off-until-boot change anyway, since it lowers the peak
draw while the touch firmware starts.</p>
<h2 id="one-tap-two-toggles">One tap, two toggles</h2>
<p>Touch now worked, and the Home Assistant logbook showed a new problem:</p>
<pre tabindex="0"><code>18:17:17  light.living_room_lamp_two  on   light.toggle
18:17:17  light.living_room_lamp_two  off  light.toggle
</code></pre><p>The GSL3680 sometimes reports a single tap as two presses a few milliseconds
apart, at almost the same coordinates. Each one fired a toggle. Every card
action now goes through a small guard:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// The GSL3680 sometimes reports one finger tap as two presses a few ms apart,
</span></span></span><span class="line"><span class="cl"><span class="c1">// which made toggle buttons flip twice. Ignore taps within 400 ms of the last.
</span></span></span><span class="line"><span class="cl"><span class="kr">inline</span> <span class="kt">bool</span> <span class="nf">tap_allowed</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">static</span> <span class="kt">uint32_t</span> <span class="n">last</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">uint32_t</span> <span class="n">now</span> <span class="o">=</span> <span class="n">esphome</span><span class="o">::</span><span class="n">millis</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">last</span> <span class="o">&lt;</span> <span class="mi">400</span><span class="p">)</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">last</span> <span class="o">=</span> <span class="n">now</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">on_click</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">if</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">condition</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">lambda</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;return tap_allowed();&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">then</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">homeassistant.action</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">light.toggle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">entity_id</span><span class="p">:</span><span class="w"> </span><span class="l">${light_1}</span><span class="w">
</span></span></span></code></pre></div><p>A longer <code>touch_timeout</code> of 150 ms on the touchscreen helped too. After that,
22 presses gave clean single toggles.</p>
<h2 id="gotchas-collected">Gotchas, collected</h2>
<ul>
<li><strong>Read the rear label first.</strong> Batch 2624 and later needs <code>JC8012P4A1-V2</code>.
Production v3 silicon needs <code>engineering_sample: false</code>.</li>
<li><strong>Flash through the port marked USB_UART.</strong> It&rsquo;s <code>/dev/ttyUSB0</code>, and logs
need <code>logger: hardware_uart: UART0</code>.</li>
<li><strong>ESPHome 2026.9.0 boot-loops on P4 v3.</strong> Vendor <code>mipi_dsi</code> with
<code>phy_clk_src</code> set to 0 until 2026.10.</li>
<li><strong>Power it properly.</strong> A weak USB port leaves the touch controller dead while
everything else looks fine. If <code>0xB0</code> reads zeros, try the power before you
try another driver.</li>
<li><strong>Allow Home Assistant actions</strong> on the ESPHome integration entry, or taps
are ignored.</li>
<li><strong>Debounce taps.</strong> The GSL3680 can split one tap into two presses.</li>
<li><strong>Recreate the venv and clear <code>.esphome/</code> after moving the project.</strong> Both
remember absolute paths.</li>
</ul>
<p>The panel now shows four lamps, four switches and the downstairs thermostat,
and everything responds to a tap. Most of the afternoon went into a touch
problem that turned out to be a USB port.</p>
]]></content:encoded></item></channel></rss>