Skip to content

Home IoT: an ESPHome fleet on a Flint-bridged VLAN

How a home IoT segment ended up shaped the way it is: a GL.iNet Flint 2 broadcasting the IoT SSID, bridged untagged into a switch port with PVID 40, onto a router-owned 10.40.0.0/24 where kea pins every device and nftables lets the segment talk to exactly one host. For anyone running ESPHome devices who has wondered whether the hub itself can double as the access point - measured answer: not this hub.

Provenance. Everything marked measured was observed on this rig, 2026-08-10 and 2026-08-11: a Raspberry Pi 4B (BCM43455 radio, Argon ONE aluminum case, NixOS) as the hub, a GL.iNet Flint 2 as the wireless access point, a managed switch, a mini PC running NixOS with kea + nftables as the edge router, and four Athom Smart Plug V3 (ESP32-C3, ESPHome) as the fleet. Claims that come from documentation rather than measurement are marked as such, and one mechanism is marked as inferred where the behavior was measured but the cause was not proven.

TL;DR:

  • A Pi 4B’s onboard radio in a metal case could not hold four ESP32 plugs in the same room: associate, handshake, disassociate, repeat, forever. This failure mode is not exotic - the same connect-then-disconnect loop on the same chip is an open linux-kernel report, and ESP32 clients specifically trigger a brcmfmac firmware crash tracked by the Raspberry Pi kernel tree (in concurrent STA+AP mode, a harder configuration than the AP-only one used here).12
  • The fix was not a better Pi antenna. It was moving the SSID to the Flint 2, whose only job here is radios, and bridging that SSID at layer 2 into a VLAN the router owns. DHCP, reservations, and firewall policy all live on the router; the Flint runs proto='none' and forwards frames.
  • The adoption path that works is the device’s own fallback hotspot plus captive portal at http://192.168.4.1 - but the fallback never fires while the old AP is still beaconing badly, because every flap-loop association counts as a connection and resets the timer. Retire the old SSID first, then the escape hatch opens by itself.34
  • Server-side DHCP reservations (kea), not ESPHome manual_ip: the pin survives reflashing and factory resets, lives in git, and makes use_address stable. Same reason you do not bake the SSID’s future into firmware you cannot reach.

IoT fleet (2.4GHz)Flint 2 (bridge only)athom-plug-110.40.0.20ra2SSID home-iotWPA2-PSK, isolate=1athom-plug-210.40.0.21athom-plug-3/410.40.0.22/.23edge routerVLAN 40 = 10.40.0.0/24kea + nftablesbr-iotproto='none'lan1managed switchport 2: access, PVID 40untaggedtrunk, 40 taggedpi-hub (hub)10.10.0.7, admin segmentHA + ESPHomeforward: 6053/3232/80 only

The Flint owns no IP on br-iot, runs no DHCP, no NAT, no firewall for this segment. It is a media converter: RF to untagged Ethernet on a port the switch has already decided is VLAN 40.

OptionCostMeasured outcomeOps burden
Pi 4B onboard BCM43455 as AP$0 (owned)Flap loop; 45-290 ms pings with loss to a plug in the same room; tx failed: 0 but multi-second RTTsTwo NixOS module traps on day one (HT capabilities, dnsmasq bind timing); every rebuild risked the AP
USB mt76 dongle on the Pi (ALFA AWUS036ACM class)~2-4x normal price (scalper listings at decision time)Not bought, so not measuredSecond radio to own; still terminates on the hub
Flint 2 (already the home WAP) hosts the SSID$0 (owned)Plug signal -70 dBm through a wall; ping across the full path 0.734/1.018/1.460 ms min/avg/max, 0% lossUCI is live state, not config-managed - see the gotcha

The Flint 2’s radios are MediaTek MT7986 (the UCI device names are mt798611/mt798612), and the stock GL firmware drives them with MediaTek’s own mt_wifi/mtk_warp modules, not mainline mt76 - measured via lsmod on the Flint. The dongle would have been mainline mt76 on the Pi. The driver differs; the point does not: known-good silicon with real antennas, in a case that is not a Faraday cage, already on the wall.

The Pi hosted pi-iot for one evening. The measured failure sequence, per device, in the hostapd journal:

wlan0: STA cc:8d:a2:00:00:01 IEEE 802.11: associated
wlan0: STA cc:8d:a2:00:00:01 WPA: pairwise key handshake completed (RSN)
wlan0: STA cc:8d:a2:00:00:01 IEEE 802.11: disassociated
wlan0: AP-STA-DISCONNECTED cc:8d:a2:00:00:01

then again, seconds later, indefinitely. Authentication succeeded; the link would not hold. This was not a passphrase or AKM problem - both WPA-PSK and WPA-PSK-SHA256 were offered, and a PMF-capable phone completed the full 4-way handshake, took a lease, and passed NAT’d traffic during the same window.

Two external data points say this was not a local misconfiguration. A linux-kernel report from April 2025 describes the same connect-then-disconnect loop on the same chip with current firmware, unresolved in the thread.1 A Raspberry Pi kernel issue tracks a BCM43455 firmware crash triggered specifically by ESP32-class clients that reconnect without clean deauth frames - exactly what a flapping plug does - though in concurrent STA+AP mode, a harder configuration than the AP-only one used here.2 The Argon ONE’s aluminum case over a PCB antenna did the rest: even when associated, the link budget left nothing.

One SSID, one VLAN, one owner per job:

JobOwnerDetail
RadiosFlint 2SSID home-iot, WPA2-PSK (psk2), 2.4GHz, client isolation on
L2 transportSwitch port 2Access port, PVID 40; the AP tags nothing
DHCP + pinskea on the routerOne reservation per device MAC, 10.40.0.20-23
Policynftables on the routerForward chain: IoT -> pi-hub only (6053 API, 3232 OTA, 80 web UI the other way), plus NTP (udp/123) to the internet. No other internet, no other segment. DNS/DHCP arrive as input to the router and are unaffected
Hubpi-hub, admin segmentHome Assistant + ESPHome at 10.10.0.7; the only host the fleet can reach

The pins, live in kea as measured from /var/lib/kea/leases4.csv:

DeviceMACPin
athom-plug-1cc:8d:a2:00:00:0110.40.0.20
athom-plug-2cc:8d:a2:00:00:0210.40.0.21
athom-plug-3cc:8d:a2:00:00:0310.40.0.22
athom-plug-4cc:8d:a2:00:00:0410.40.0.23

Reservations over manual_ip, deliberately: an address baked into firmware needs a reflash to change and dies on a factory reset, while a server-side pin lives in git, survives reflashing, and keeps one list of who-is-who. The pin is also what makes ESPHome’s use_address stable - mDNS across segments cannot be relied on.

The Flint’s stock IoT network is its own little router: NAT’d 192.168.10.0/24 with a DHCP server that turns itself on when the network is enabled.5 Left as-is it would have fought kea for every DHCP discover on the segment. The conversion is four UCI statements, measured live:

  • network.iot.proto='none' - the interface becomes an unmanaged bridge member, no address, no routing
  • dhcp.iot.ignore='1' - the stock dnsmasq instance stands down
  • lan1 moves from br-lan to br-iot - the wired uplink for the segment
  • wireless.iot2g: enabled, ssid='home-iot', encryption='psk2', same passphrase the fleet already knew, isolate='1' kept

The L2 proof was a temporary address on the Flint’s bridge: ip addr add 10.40.0.250/24 dev br-iot, ping the router at 10.40.0.1 (3/3, rtt min/avg/max 0.734/1.018/1.460 ms), ARP resolved to the router’s MAC, then delete the address. That exercises the entire path - lan1, switch PVID, trunk, VLAN interface - with no config left behind.

The plan was to OTA each plug with the new SSID while it sat on the old one. The flap loop made that untenable - an OTA needs minutes of stable link, and the plugs could not hold seconds. The path that worked needs no link to the old AP at all:

  1. Switch the old AP off. This is the step the docs do not spell out. ESPHome opens its fallback hotspot only after ap_timeout with no successful connection - but a flap loop produces a successful association every few seconds, which resets the timer, so the fallback never fires while a sick AP keeps beaconing. Observed: over an hour of flapping with the old AP up produced no fallback SSID; once the old AP was off (and each plug had rebooted into the silence), the fallback SSIDs appeared within minutes. The timer-reset mechanism itself is inferred from the ESPHome source and docs, not proven on this rig.3
  2. Join the fallback hotspot from a phone, open http://192.168.4.1, enter the new SSID and passphrase. The captive portal stores the credentials in flash and the device reboots onto the new network.4
  3. The pin does the rest. kea recognizes the MAC, hands over the reserved address, and the device appears at exactly the use_address the dashboard already points at.
  4. OTA once, over the good link, to bake the new SSID into firmware. Portal-stored credentials survive reboots but a later serial flash would overwrite them; the bake keeps firmware and YAML saying the same thing.4

Two measured surprises on this path: the stock Athom firmware accepted the OTA of the replacement config with no password prompt, and the portal-set credentials survived every reboot without the bake - but the bake is still worth doing for the YAML-firmware match.

Claim in this docHow it was checked
Flap loop: associate / handshake completed / disassociated, repeatingMeasured 2026-08-10/11: hostapd journal on pi-hub, both plug MACs
45-290 ms pings with loss to a same-room plug on the Pi APMeasured 2026-08-10: ping from pi-hub over pi-iot
Pi AP verified working for a phone (handshake, lease, NAT flows)Measured 2026-08-10: hostapd journal + dnsmasq leases + /proc/net/nf_conntrack ASSURED entries
New path latency 0.734/1.018/1.460 ms, 0% lossMeasured 2026-08-11: temporary address on br-iot, ping 10.40.0.1
Plug signal -70 dBm on the FlintMeasured 2026-08-11: ESPHome log stream, athom-plug-1 WiFi Signal sensor
All four kea pins taken by the correct MACsMeasured 2026-08-11: /var/lib/kea/leases4.csv on the router
Pi -> plug :6053 and :3232 reachable through the forward ruleMeasured 2026-08-11: TCP connect from pi-hub to 10.40.0.20/.21/.23
Stock Athom OTA accepts the replacement config, no passwordMeasured 2026-08-11: dashboard Install to 10.40.0.22/.23
Fallback hotspot never fires during the flap loop; fires once the old AP is offMeasured behavior 2026-08-11; timer-reset mechanism inferred from ESPHome docs and source
brcmfmac cannot maintain an AP on this chipDocumented: linux-kernel thread, same chip, same symptom shape; ESP32-triggered firmware crash in raspberrypi/linux#7092 (concurrent STA+AP mode there; AP-only here)
Flint radios are MT7986 driven by MediaTek mt_wifi/mtk_warp, not mainline mt76Measured 2026-08-11: lsmod on the Flint
Stock GL IoT network NATs 192.168.10.0/24 and auto-enables its DHCP serverDocumented: GL.iNet IoT Network guide; dhcp config with force measured in the Flint’s UCI
Switch port 2 is access PVID 40Documented in the infra repo’s switch migration record; live CLI re-verification was blocked by a switch login lockout at write time
  • The hub is not the AP. A gateway radio in a metal case, an inch from a heatsink, running a driver with open AP-mode bugs, is three problems renting one antenna. Any dedicated WAP with its radios in the clear beats it, and if the WAP is already on the wall the marginal cost is four UCI statements.
  • Bridge, don’t route, at the AP. proto='none' plus a PVID’d switch port keeps DHCP, reservations, DNS, and policy in one place on the router. The AP becomes replaceable: any box that can bridge an SSID to a wire fits the slot.
  • Pins on the server, not the device. Every property baked into firmware is a property you change by reflashing. Every property on the DHCP server is a line in git.
  • The escape hatch has a precondition. ESPHome’s fallback hotspot is the right adoption path for devices you cannot reach - but it only opens when the device stays disconnected. A sick AP that almost works is worse than no AP: it keeps the device connected just enough to never ask for help.
  1. KeithG, “Cannot maintain an ap with brcmfmac,” linux-kernel mailing list, April 2025. https://lists.openwall.net/linux-kernel/2025/04/20/201 2

  2. mshioji, “BCM43455 firmware crash with concurrent STA+AP mode on kernel 6.12,” raspberrypi/linux issue 7092, filed 2025-10-17. https://github.com/raspberrypi/linux/issues/7092 2

  3. ESPHome, “WiFi Component,” ESPHome Docs. https://esphome.io/components/wifi/ 2

  4. ESPHome, “Captive Portal,” ESPHome Docs. https://esphome.io/components/captive_portal/ 2 3

  5. GL.iNet, “IoT Network,” GL.iNet Router Docs 4. https://docs.gl-inet.com/router/en/4/interface_guide/iot_network/