Skip to content

MQTT discovery: ha-paneld-aid-<ANDROID_ID> device identifier is not unique on cloned panel images — 12 panels merged into one Home Assistant device #155

Description

@cb2206

ha-paneld version

0.9.7 (build 776). The identifier involved has been published since v0.9.5 (commit 9a5d3c8b, "anchor MQTT discovery identity") and is unchanged in v0.9.8-rc1.

Panel hardware

Sonoff NSPanel Pro (86P / 120P, PX30 / rk3326)

What happened?

publishDiscovery() sends two device identifiers (v0.9.7, MqttBridge.kt lines 3822-3823):

val aid = config.androidId
val ids = if (aid.isNotBlank()) """["ha-paneld-$panel","ha-paneld-aid-$aid"]""" else """["ha-paneld-$panel"]"""

config.androidId is Settings.Secure.ANDROID_ID (also used as serial_number). The comment above it states the intent — "HA merges a device on ANY matching identifier, so a later panel_id change re-attaches to the SAME HA device instead of minting a duplicate."

That's exactly what makes it hazardous when ANDROID_ID isn't unique. On Android 8.1 the app-visible ANDROID_ID is per-signing-key but derived from a per-device seed, and on panels flashed from the same factory image that seed is cloned — so every panel hands ha-paneld the same value, and Home Assistant merges them all into one device.

Measured across my 16 NSPanel Pro units:

nsp1   7581153b255d0538      nsp9    7581153b255d0538
nsp2   7581153b255d0538      nsp10   7581153b255d0538
nsp3   7581153b255d0538      nsp11   5c39f66682346472
nsp4   7581153b255d0538      nsp12   7581153b255d0538
nsp5   7581153b255d0538      nsp13   7581153b255d0538
nsp6   7581153b255d0538      nsp14   e637ddadb4031a59
nsp7   7581153b255d0538      nsp15   7581153b255d0538
nsp8   fe3b887884df3ce7      nsp16   94e5ab44eec84a4

12 of 16 share one value. (The value ha-paneld publishes is bc9265382fac70fe rather than 7581153b255d0538 — SSAID is per-signing-key — but it derives from the same cloned seed, so it is equally identical across those 12.) The Home Assistant device registry matches that split exactly: the MQTT integration has 5 paneld-* devices instead of 16, and the one named paneld-nsp3 carries 216 entities belonging to 12 different panels. Its identifier list:

["mqtt","ha-paneld-aid-bc9265382fac70fe"] ← published identically by all 12
["mqtt","ha-paneld-ha_paneld_nsp1"]
["mqtt","ha-paneld-ha_paneld_nsp2"]
["mqtt","ha-paneld-ha_paneld_nsp3"]  … nsp4, nsp5, nsp6, nsp7, nsp9, nsp10, nsp12, nsp13, nsp15
["mqtt","ha-paneld-px30_evb_70fe"]        ← legacy auto-generated panel id

Only nsp8, nsp11, nsp14 and nsp16 — the four with distinct values — have their own device.

Consequences:

  • Every entity from 11 of those panels is named after nsp3: update.ha_paneld_nsp1_ha_companion has friendly name "paneld-nsp3 HA Companion". Settings → Updates shows the same panel name a dozen times over, which is how I found this.
  • Per-panel areas, device pages, device automations and any device_id-based targeting are impossible for those 11 panels. suggested_area only ever applied to the first registration.
  • It reaches the default panel id too: that's derived from model + the last four hex of ANDROID_ID (px30_evb_70fe above), so a cloned fleet collides there as well unless explicit panel ids are set.

There's no workaround from the user side. Deleting the merged device in Home Assistant doesn't help — the panels re-announce the same identifier on the next connect and re-merge immediately. ANDROID_ID isn't practically changeable on Android 8.1 (per-signing-key SSAID). It needs a change in ha-paneld.

Possible fixes, roughly in order of preference:

  1. Mint a random id once on first run and persist it in ha-paneld's own settings, and publish that as the second identifier. It's genuinely unique, survives a panel_id change (the original goal), and is immune to image cloning. It would need excluding from fleet restore/export the way other device-scoped keys already are, or restored panels would clone it again.
  2. Use ro.serialno, which is unique on this hardware — I checked three panels and each has a distinct serial, including two that share the cloned ANDROID_ID. ha-paneld has root on these panels, though I don't know how dependable that property is across the whole supported hardware range.
  3. Drop ha-paneld-aid-<id> from identifiers and keep ANDROID_ID only as serial_number, where it's informational and can't merge anything. That gives up the re-attach-on-rename property, which is what the pre-0.9.5 behaviour did anyway.

A validity check would also help regardless of which route is taken: if the value is blank, a well-known constant, or otherwise not unique, don't publish it as an identifier.

Whatever lands will need a note that existing merged devices must be deleted in Home Assistant by hand once panels stop publishing the shared identifier, so each one re-registers on its own.

Steps to reproduce

  1. Two or more panels flashed from the same factory image, reporting the same value for adb shell settings get secure android_id.
  2. Give each a distinct panel_id and friendly name; point them at the same broker.
  3. Settings → Devices & services → MQTT: a single device holds every panel's entities, named after whichever registered first.

Diagnostic dump (/diag)

One of the merged panels:

ha-paneld diagnostics — 0.9.7 (build 776)
[captured] 2026-09-15T10:31:18.509-06:00 uptime=2d4h

[panel]
ha-paneld=0.9.7 (build 776)
Android=8.1.0 (API 27)
Firmware=3.8.0
Device=rockchip px30_evb (px30_evb)
CPU=4 cores · arm64-v8a · rk30board
RAM=1.9 GB total · 1.2 GB free
Storage=7.8 GB eMMC · 2.2 GB free (data)
Display=750×1334 px · logical 320 dpi
System WebView=com.android.webview 138.0.7204.63
HA Companion=not installed
MQTT state=connected · TCP/IPv4 · last-ok 29s ago · last-auth 101437s ago · auth-ok · family Automatic (next IPv4)
Security mode=Relaxed
Keep panel responsive=on · power locks held
Prevent idle dim=on · timeout never
Android dashboard lock=off
LED=none
Nav actions (a11y)=yes
Navbar=Swipe reveal
Zigbee=vendor-native · running · Repeater
Relays=none
Network ADB=active (5555) · external — not persisted by ha-paneld
Audio playback=idle
App database=6.3 MB used · 7.3 MB on disk · schema 14
Product version=NSPanel120P_3.8.0
Local-state sync=449s ago · brightness 222→226 · 569s ago · brightness 218→222 · 869s ago · brightness 223→218 · 989s ago · brightness 219→223 · 1229s ago · brightness 215→219 · 1709s ago · brightness 211→215 · 2129s ago · brightness 207→211 · 2369s ago · brightness 202→207
State convergence=43 channels · 0 dirty · 0 in-flight · 15 unknown · ack 905/0 · pending none

[build] fingerprint=rockchip/px30_evb/px30_evb:8.1.0/OPM8.190605.003/092718:userdebug/test-keys
board=rk30sdk product=px30_evb hardware=rk30board abis=arm64-v8a,armeabi-v7a,armeabi
[boot-security] verified=green flash=locked vbmeta=unknown build=userdebug debuggable=yes
[env] selinux=0 su=true write_settings=true a11y=true daemon=true shizuku=manager_missing evdev=stopped/none ledjni=false
[display-sizing] android_base_logical_dpi=240 current_logical_dpi=320 override_dpi=320 font_scale=1.0 profile_recommended_dpi=250 profile_recommended_font_scale=1.0
[renderer] mode=builtin state=checking outcome=unobserved fault=none detail=none recovery=none transport=http address_family=automatic observed_age=49m process_age=1d4h package_updated=1d4h theme=follow/light
[ha-network] state=healthy cause=none responsiveness=healthy measuring=true socket=live window=5m probes=30 round_trips=30 p50=34 p95=123 max=137 jitter=27 loss=0.0% consecutive=0 server_errors=0 auth_errors=0 last_reply_age=4734
[ha-path-probe] state=healthy family=ipv4 bursts=6 sent=30 received=30 loss=0.0% p50=23 p95=58 max=64 jitter=19 dead_bursts=0 interval=600s last_burst_age=155448
[camera] state=absent outcome=camera-unavailable fault=none detail=not_enumerated recovery=none clients=0 last_frame=never failures=0 indication=none stream_clients=0 stream_port=off encoder=none encode=none delivered=none requested_fps=none
[zigbee-health] state=unknown layout=unknown package=- joined=true role=Repeater gateway_cpu=4 guard_cpu=0 restarts_10m=0 containment=none recursive_watchdog=false
[storage-health] state=healthy pressure_state=healthy usable_bytes=2176237568 total_bytes=3606069248 used_percent=39.6506994643271 database_bytes=54165504 wal_bytes=524288 sidecar_bytes=163840 page_size_bytes=4096 page_count=13224 freelist_count=2 schema_version=14 quick_check=ok auto_vacuum=incremental checked_at=1789474827801 failure=none failure_operation=none
[power-safety] state=safe keep_awake=true wake_lock=true wifi_lock_required=true wifi_lock=true prevent_idle_dim=true screen_timeout_ms=2147483647 interactive=true power_source=ac plugged_mask=1 stay_on_mask=15 stay_on_effective=true device_idle=false doze_exempt=true screen_off=su_blpower screen_off_selected=unknown screen_off_reason="not yet exercised; no screen-off has occurred since this controller was constructed" reasons=none
[sysfs] leds=[] backlight=[backlight] devfreq=[dmc, ff400000.gpu]
[labels] ls: /sys/class/leds/*/: No such file or directory ls: /dev/ledjni: No such file or directory u:object_r:sysfs:s0 /sys/class/backlight/backlight/
[hardware]
  inputs=[rk8xx_pwrkey (event0 cpufreq dmcfreq keychord ), chsc_cap_touch (event1 cpufreq dmcfreq ), adc-keys (event2 cpufreq dmcfreq keychord ), gpio_keys (event3 cpufreq dmcfreq ), lightsensor-level (event4 ), proximity (event5 )]
  i2c=[0-0020:rk809, 1-002e:chsc_cap_touch, 1-003c:tp, 2-0046:ls_stk3a5x, 2-0046-1:ps_stk3a5x, 5-0038:px30-aht20, i2c-0:rk3x-i2c, i2c-1:rk3x-i2c, i2c-2:rk3x-i2c, i2c-5:gpio_i2c@3]
  iio=[]
  thermal=[cooling_device0:thermal-cpufreq-0, cooling_device1:thermal-devfreq-0, cooling_device2:thermal-devfreq-1, thermal_zone0:soc-thermal, thermal_zone1:gpu-thermal]
  relays=[st_relay/mode, st_relay/relay1, st_relay/relay2, st_relay/relay3, st_relay/relay4]
[packages] android=not installed minimal=not installed
[vendor-tame] known=7 installed=7 active=0 disabled=7
[capabilities] Root (su)=ok | Verified app update / screenshot / display=ok | Screen brightness=ok | Screen on/off=ok | Reboot / reload / launcher=ok

Home Assistant version

2026.9.2

Relevant logs

Nothing logged on either side — MQTT discovery is working as written; the identifier just isn't unique.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions