fix: IP-independent pairing key fallback; run commands off the paho thread

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-10-23 00:34:53 -04:00
parent f7422c9d74
commit 34adf2848b
3 changed files with 25 additions and 0 deletions
+2
View File
@@ -51,6 +51,8 @@ Format: `YYYY-MM-DD: <decision> — <why>`
- 2026-10-23: TV DHCP-reserved at 192.168.0.20 (Omada) — bscpylgtv keys pairings by IP, so every DHCP drift (7 IPs so far) re-triggered the on-TV auth prompt, and a missed prompt hung `off` past the 60s timeout. New IP seeded with the newest stored key (.31) so no re-prompt. Also: on_message now catches all exceptions — one escaping killed paho's thread while the poll loop kept the process alive (silent zombie).
- 2026-10-23 (later): Reservation alone didn't hold — the TV came back on .45 minutes later and prompted again. lgtv.sh now falls back to the newest stored key when there's no key for the current IP, so IP drift is harmless. Bridge commands run on a single worker thread so a 60s `off` can't starve paho's keepalive and drop queued commands.
## Deferred / Rejected
- **DHCP reservation for the TV (static IP):** not done — Pi-hole doesn't run
DHCP (router does), so it's Seth's action on the router. The cached-IP probe
+13
View File
@@ -153,8 +153,21 @@ from bscpylgtv import WebOsClient
# = works regardless of cwd/env. Override with LGTV_KEY_FILE if needed.
KEY = os.environ.get('LGTV_KEY_FILE') or os.path.expanduser('~/.aiopylgtv.sqlite')
# bscpylgtv stores keys per IP, but the TV doesn't care which IP a key comes
# from. Without this, every DHCP drift = a fresh on-TV pairing prompt.
def newest_key():
import pickle, sqlite3
try:
row = sqlite3.connect(KEY).execute(
"SELECT value FROM unnamed ORDER BY rowid DESC LIMIT 1").fetchone()
return pickle.loads(row[0]) if row else None
except sqlite3.Error:
return None
async def main():
client = await WebOsClient.create(os.environ['TV_IP'], key_file_path=KEY)
if client.client_key is None:
client.client_key = newest_key()
await client.connect()
await client.power_off()
await asyncio.sleep(1)
+10
View File
@@ -18,6 +18,9 @@ import time
import json
import logging
import paho.mqtt.client as mqtt
from concurrent.futures import ThreadPoolExecutor
worker = ThreadPoolExecutor(max_workers=1)
# --- Config ---
MQTT_HOST = "192.168.0.154" # CT 301 Mosquitto
@@ -110,6 +113,13 @@ def on_connect(client, userdata, flags, rc):
def on_message(client, userdata, msg):
# Hand off to one worker thread: an `off` can block up to 60s, which starves
# paho's keepalive (broker drops us, queued commands are lost). The single
# worker keeps commands in order.
worker.submit(safe_handle, client, msg)
def safe_handle(client, msg):
# An exception escaping a paho callback kills the network thread while the
# poll loop keeps the process alive — a silent zombie systemd never restarts.
try: