#!/usr/bin/env python3 """Persistent Vidaa TV bridge: line-JSON commands on stdin, line-JSON responses/events on stdout. Usage: bridge.py """ import sys import json import asyncio import socket import struct import threading from pathlib import Path from typing import Optional import os VENV_SITE = os.environ.get("VIDAA_SITE") or str(Path(__file__).resolve().parent.parent / ".venv/lib/python3.9/site-packages") if VENV_SITE not in sys.path: sys.path.insert(0, VENV_SITE) from vidaa import AsyncVidaaTV from vidaa.config.storage import TokenStorage HOST = sys.argv[1] if len(sys.argv) > 1 else "192.168.1.146" MAC = sys.argv[2] if len(sys.argv) > 2 else "" STORE = sys.argv[3] if len(sys.argv) > 3 else str(Path.home() / "Library/Application Support/HisenseBar/tokens.json") tv: Optional[AsyncVidaaTV] = None loop: Optional[asyncio.AbstractEventLoop] = None out_lock = threading.Lock() pending = 0 eof = False def emit(obj: dict): with out_lock: print(json.dumps(obj), flush=True) def wake(): addr = MAC.replace(":", "").replace("-", "") if len(addr) != 12: return False pkt = b"\xff" * 6 + bytes.fromhex(addr) * 16 for bcast in ("255.255.255.255", ".".join(HOST.split(".")[:3]) + ".255"): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) s.sendto(pkt, (bcast, 9)) s.close() except OSError: pass return True async def get_tv() -> AsyncVidaaTV: global tv if tv is None: storage = TokenStorage(Path(STORE)) tv = AsyncVidaaTV( HOST, mac_address=MAC or None, use_dynamic_auth=bool(MAC), storage=storage, on_state_change=lambda d: emit({"event": "state", "data": d}), ) return tv async def handle(c: dict) -> dict: cmd = c.get("cmd") t = await get_tv() if cmd == "connect": ok = await t.async_connect(timeout=8, auto_auth=True) return {"ok": ok} if cmd == "pair_start": ok = await t.async_start_pairing() return {"ok": ok} if cmd == "pair_auth": ok = await t.async_authenticate(str(c["pin"]), timeout=15) return {"ok": ok} if cmd == "key": await t.async_send_key(c["key"]) return {"ok": True} if cmd == "volume": v = await t.async_get_volume() return {"ok": v is not None, "volume": v} if cmd == "set_volume": await t.async_set_volume(int(c["value"])) return {"ok": True} if cmd == "mute": await t.async_mute() return {"ok": True} if cmd == "apps": apps = await t.async_get_apps() return {"ok": apps is not None, "apps": apps or []} if cmd == "launch": await t.async_launch_app(c["app"]) return {"ok": True} if cmd == "sources": srcs = await t.async_get_sources() return {"ok": srcs is not None, "sources": srcs or []} if cmd == "set_source": payload = {"sourceid": str(c["source"])} if c.get("name"): payload["sourcename"] = str(c["name"]) def _pub(): cl = t._ensure_client() from vidaa.topics import get_topic, TOPIC_SET_SOURCE return cl._publish(get_topic(TOPIC_SET_SOURCE, cl.client_id), payload) await t._run_in_executor(_pub) return {"ok": True} if cmd == "state": st = await t.async_get_state() return {"ok": st is not None, "state": st} if cmd == "power_off": await t.async_power_off() return {"ok": True} if cmd == "wake": return {"ok": wake()} return {"ok": False, "error": f"unknown cmd {cmd}"} def stdin_reader(): for line in sys.stdin: line = line.strip() if not line: continue try: msg = json.loads(line) except json.JSONDecodeError: continue rid = msg.get("id") global pending pending += 1 fut = asyncio.run_coroutine_threadsafe(handle(msg), loop) try: res = fut.result(timeout=45) except Exception as e: res = {"ok": False, "error": f"{type(e).__name__}: {e}"} res["id"] = rid emit(res) pending -= 1 global eof eof = True if pending == 0: import os os._exit(0) async def main(): global loop loop = asyncio.get_running_loop() threading.Thread(target=stdin_reader, daemon=True).start() emit({"event": "ready"}) while True: await asyncio.sleep(3600) if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass