Files
lgtv/.claude/handoffs/2026-10-23-003809-pairing-fix-verified.md
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

129 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](./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
- [x] Diagnosed the zombie bridge (paho thread died on TimeoutExpired; the poll loop masked it)
- [x] Diagnosed the per-IP key problem (TV has drifted through 11 IPs)
- [x] Omada reservation f8:01:b4:2a:26:ed → .20 (set, but the TV took .45 anyway)
- [x] Key fallback in lgtv.sh, worker thread + exception guard in lgtv_mqtt.py, deployed
- [x] 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).
## Related Resources
- `DECISIONS.md` (2026-10-23 entries)
- Previous handoffs: `2026-10-22-ha-lost-ipv4.md`, `2026-10-23-pairing-prompt-ip-drift.md`