Files
lgtv/.claude/handoffs/2026-10-23-003809-pairing-fix-verified.md
T
Mortdecai 2f96013794 docs: handoff — pairing fix verified on/off/on
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 00:38:40 -04:00

5.7 KiB
Raw Blame History

Handoff: pairing-prompt / zombie-bridge fix — verified on→off→on

Session Metadata

  • Created: 2026-10-23 00:38:09
  • Project: /home/claude/bin/lgtv
  • Branch: master
  • Session duration: ~25 min (00:15–00:40)

Recent Commits (for context)

  • 9d6f2d2 docs: handoff — pairing prompt / IP drift / zombie bridge
  • 34adf28 fix: IP-independent pairing key fallback; run commands off the paho thread
  • f7422c9 fix: survive command exceptions; TV DHCP-reserved so pairing key stays valid
  • df0ae87 docs: HA VM lost IPv4 — root cause of months-long TV outage; pinned .146

Handoff Chain

  • Continues from: 2026-10-23-pairing-prompt-ip-drift.md
    • Previous title: pairing prompt on every IP change + zombie bridge
  • Supersedes: 2026-10-23-pairing-prompt-ip-drift.md (its "Open" items are restated here)

Current State Summary

Google Home → HA → MQTT → lgtv bridge on steel141 works end to end. Seth tested ON → OFF → ON at 00:37 with no TV prompt, and the journal shows three commands received, all rc=0. That closes both outages: the 2026-10-22 HA IPv4 loss and tonight's pairing-prompt/zombie-bridge bug. Nothing is in progress.

Codebase Understanding

Architecture Overview

Google Home → HA (VM 706, .146) → Mosquitto (.154) → lgtv_mqtt.py (systemd lgtv-mqtt.service, User=seth, steel141) → shells out to lgtv.sh on|off|status. ON = Wake-on-LAN (no key needed). OFF = bscpylgtv WebSocket power_off (needs a pairing key). Status = TCP probe of :3000/:3001 at the cached IP, falling back to a /24 sweep matched by MAC.

Critical Files

File Purpose Relevance
lgtv_mqtt.py MQTT↔HA bridge Commands now run on a 1-thread ThreadPoolExecutor, wrapped in try/except
lgtv.sh on/off/status newest_key() fallback when no key is stored for the current IP
/home/seth/.aiopylgtv.sqlite bscpylgtv key store (keyed by IP, pickled values) 11 rows; newest = .45
/home/seth/.cache/lgtv/last_ip cached TV IP currently 192.168.0.45
install.sh deploy → /usr/local/bin/ re-run after editing either script

Key Patterns Discovered

  • bscpylgtv async_init loads client_key from storage by IP at create() time. Setting client.client_key before connect() overrides it, and a key the TV issues is saved under the current IP (webos_client.py:237).
  • Never block inside a paho callback. An exception kills the network thread, and a long block starves the keepalive, so QoS 0 commands get dropped.

Work Completed

Tasks Finished

  • Diagnosed the zombie bridge (paho thread died on TimeoutExpired; the poll loop masked it)
  • Diagnosed the per-IP key problem (TV has drifted through 11 IPs)
  • Omada reservation f8:01:b4:2a:26:ed → .20 (set, but the TV took .45 anyway)
  • Key fallback in lgtv.sh, worker thread + exception guard in lgtv_mqtt.py, deployed
  • Seth verified ON→OFF→ON via Google Home

Files Modified

File Changes Rationale
lgtv_mqtt.py worker thread + safe_handle survive and not block on slow/failed commands
lgtv.sh newest_key() fallback IP drift no longer forces a re-pair
DECISIONS.md two 2026-10-23 entries record the why

Decisions Made

Decision Options Considered Rationale
Newest-key fallback instead of relying on the reservation reservation only; pass a pinned client_key from env The reservation didn't hold; the fallback self-updates as the TV issues new keys
Single worker thread thread per command; asyncio Keeps command order; smallest diff

Pending Work

Immediate Next Steps

  1. Nothing required. If a prompt reappears, check last_ip against the newest sqlite row. A prompt at a new IP means the TV rejected the fallback key.
  2. Optional: find out why the Omada reservation for .20 wasn't applied (the TV got .45 at 00:31). Either fix it, or re-reserve at .45 to match reality.

Blockers/Open Questions

  • Still unverified: whether the TV accepts an old key from a new IP. The TV stayed at .45 during the test, so the fallback path never ran. The first real IP change will tell.
  • Why didn't the reservation stick? Unknown.

Deferred Items

  • Reducing the 60s off timeout: deferred. It no longer harms the bridge, it only delays the next command.

Context for Resuming Agent

Important Context

  • The service runs as seth, so the key store and cache are under /home/seth. Edit them as sudo -u seth.
  • The ON path never needs a key. Any "can't turn on" bug sits upstream of WoL (HA, MQTT, a lost message) or is the TV's WoL setting.
  • The bridge reports ON while the TV keeps :3000 open in network standby, so HA state can lag after OFF.

Assumptions Made

  • LG validates the key itself, not the source IP (consistent with the protocol; not yet seen across an IP change).

Potential Gotchas

  • A long-running process with a dead paho thread still looks healthy in systemctl status. Check the journal for Exception in thread paho.
  • Backups of pre-edit files are in lgtv/.backup/ (also the sqlite store as of 00:2x).

Environment State

Tools/Services Used

  • network-tools/omada_reserve_ip.py show|reserve <MAC> [IP]
  • journalctl -u lgtv-mqtt.service

Active Processes

  • lgtv-mqtt.service on steel141, restarted 00:34:40 with the current code.

Environment Variables

  • LGTV_KEY_FILE, LGTV_STATE, TV_IP (set internally), MQTT creds from the project env file (names only).
  • DECISIONS.md (2026-10-23 entries)
  • Previous handoffs: 2026-10-22-ha-lost-ipv4.md, 2026-10-23-pairing-prompt-ip-drift.md