Files
lgtv/lgtv_mqtt.py
T
Mortdecai 1699276e8e feat: consolidate LG TV bridge — cached IP discovery, robust WoL, systemd deploy
Rework the MQTT bridge into a single deployable unit:
- Drop the self-SSH hop; run locally on steel141 (on the TV's LAN, has
  bscpylgtv, effectively always-on).
- Cache last-known TV IP and probe it directly; /24 ping-sweep only on a
  cache miss. Stops the 30s HA state poll from flapping/spamming the net.
- WoL now unicast (last-known IP) + broadcast, ports 9/7, 3 bursts.
- systemd unit (User=seth, EnvironmentFile for MQTT_PASSWORD), install.sh,
  env template, .gitignore. Removes the half-finished lgtv.sh.new.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:29:01 -04:00

164 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
LG TV MQTT bridge for Home Assistant.
Runs locally on steel141 as user `seth` (where bscpylgtv lives) via systemd.
Subscribes to command topic, runs lgtv.sh, publishes state back.
Registers the TV as a switch via MQTT discovery so HA picks it up automatically.
Topics:
homeassistant/switch/lgtv/config -- discovery payload (published on connect)
lgtv/switch/state -- current state: ON / OFF
lgtv/switch/command -- receives: ON / OFF
"""
import os
import subprocess
import time
import json
import logging
import paho.mqtt.client as mqtt
# --- Config ---
MQTT_HOST = "192.168.0.154" # CT 301 Mosquitto
MQTT_PORT = 1883
MQTT_USER = "lgtv"
MQTT_PASS = os.environ.get("MQTT_PASSWORD", "")
LGTV_SCRIPT = "/usr/local/bin/lgtv.sh"
DISCOVERY_TOPIC = "homeassistant/switch/lgtv/config"
STATE_TOPIC = "lgtv/switch/state"
COMMAND_TOPIC = "lgtv/switch/command"
POLL_INTERVAL = 30 # seconds between state polls
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [lgtv-mqtt] %(levelname)s %(message)s",
)
log = logging.getLogger(__name__)
def run(cmd, timeout=60):
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=timeout
)
return result.stdout.strip(), result.returncode
def get_tv_state():
"""Returns 'ON' or 'OFF' by using the same probe logic as lgtv.sh status."""
out, rc = run(f"{LGTV_SCRIPT} status")
if rc != 0:
log.warning(f"status probe failed rc={rc} out={out}")
return "OFF"
return "ON" if "TV: ON" in out else "OFF"
def tv_on():
out, rc = run(f"{LGTV_SCRIPT} on")
log.info(f"tv_on: rc={rc} out={out}")
return rc == 0
def tv_off():
out, rc = run(f"{LGTV_SCRIPT} off")
log.info(f"tv_off: rc={rc} out={out}")
return rc == 0
def publish_discovery(client):
payload = {
"name": "LG TV",
"unique_id": "lgtv_f801b42a26ed",
"state_topic": STATE_TOPIC,
"command_topic": COMMAND_TOPIC,
"payload_on": "ON",
"payload_off": "OFF",
"state_on": "ON",
"state_off": "OFF",
"optimistic": False,
"device": {
"identifiers": ["lgtv_f801b42a26ed"],
"name": "LG TV",
"manufacturer": "LG",
"model": "WebOS TV",
},
"icon": "mdi:television",
"availability_topic": "lgtv/switch/availability",
"payload_available": "online",
"payload_not_available": "offline",
}
client.publish(DISCOVERY_TOPIC, json.dumps(payload), retain=True)
client.publish("lgtv/switch/availability", "online", retain=True)
log.info("Published MQTT discovery payload")
def on_connect(client, userdata, flags, rc):
if rc == 0:
log.info("Connected to MQTT broker")
publish_discovery(client)
client.subscribe(COMMAND_TOPIC)
log.info(f"Subscribed to {COMMAND_TOPIC}")
# Publish current state on connect
state = get_tv_state()
client.publish(STATE_TOPIC, state, retain=True)
log.info(f"Initial state: {state}")
else:
log.error(f"MQTT connect failed: rc={rc}")
def on_message(client, userdata, msg):
command = msg.payload.decode().strip().upper()
log.info(f"Received command: {command}")
if command == "ON":
tv_on()
# Publish ON optimistically immediately — WoL takes ~20s to appear on network.
# The 30s poll loop will correct it if it failed.
client.publish(STATE_TOPIC, "ON", retain=True)
log.info("State published: ON (optimistic)")
elif command == "OFF":
tv_off()
time.sleep(3)
client.publish(STATE_TOPIC, "OFF", retain=True)
log.info("State published: OFF")
else:
log.warning(f"Unknown command: {command}")
return
def main():
client = mqtt.Client(client_id="lgtv-bridge")
client.username_pw_set(MQTT_USER, MQTT_PASS)
client.on_connect = on_connect
client.on_message = on_message
client.will_set("lgtv/switch/availability", "offline", retain=True)
log.info(f"Connecting to {MQTT_HOST}:{MQTT_PORT}...")
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
client.loop_start()
last_poll = 0
try:
while True:
now = time.time()
if now - last_poll >= POLL_INTERVAL:
state = get_tv_state()
client.publish(STATE_TOPIC, state, retain=True)
log.info(f"Polled state: {state}")
last_poll = now
time.sleep(5)
except KeyboardInterrupt:
log.info("Shutting down")
finally:
client.publish("lgtv/switch/availability", "offline", retain=True)
client.loop_stop()
client.disconnect()
if __name__ == "__main__":
main()