#!/usr/bin/env python3 # QNAP front-panel LCD menu for esh-nas-pve (QNAP hardware running Proxmox VE, not QTS). # # Replaces the menu script of DigitalFunk/QnapLCD-Proxmox (checked out at /usr/local/bin/qnap_lcd, commit f2c9c46), # whose "Proxmox" fork still opened on the TrueNAS screen: menu[0] ran `cli -c 'system version'`, a TrueNAS-only # command, so the script died on its first screen and the panel stayed on "Initializing..." with dead buttons. # The same script also hid the management address: it skipped every bridge, and on Proxmox the IP lives on vmbr0. # Uses the repo's own `qnaplcd` driver unchanged (A125 display, 2x16, /dev/ttyS1 @ 1200 baud). # Source of truth: eshpfi services/qnap-lcd/ (deployed by playbooks/qnap-lcd.yaml). Test without the panel: # python3 lcd-menu.py --dry-run (prints every screen instead of writing to the LCD) import json import os import platform import subprocess import sys import threading import time sys.stdout.reconfigure(line_buffering=True) DISPLAY_TIMEOUT = 30 # seconds the backlight stays on after a button press REFRESH = 30 # seconds between redraws of the current screen PORT, PORT_SPEED = '/dev/ttyS1', 1200 WIDTH = 16 def shell(cmd): return subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.DEVNULL).strip() def fit(text): return str(text)[:WIDTH] # ---------- screens: each returns two lines ---------- def host_ip(): addrs = ipv4_addresses() for name, ip in addrs: if name == 'vmbr0': return ip return addrs[0][1] if addrs else 'no IPv4' def screen_host(): return [platform.node(), host_ip()] def screen_pve(): v = shell('pveversion') # pve-manager/9.2.21/4f6e0ac8 (running kernel: 7.0.14-20-pve) ver = v.split('/')[1] if '/' in v else v return [f'PVE {ver}', platform.release()] # 'Proxmox VE 9.2.21' is 17 chars, one too many for the panel def screen_uptime(): secs = int(float(open('/proc/uptime').read().split()[0])) d, rem = divmod(secs, 86400) h, rem = divmod(rem, 3600) m = rem // 60 up = f'{d}d {h:02d}:{m:02d}' if d else f'{h:02d}:{m:02d}' load = os.getloadavg() return [f'Up {up}', f'Ld {load[0]:.2f} {load[1]:.2f}'] def ipv4_addresses(): out = [] for iface in json.loads(shell('ip -json address show')): name = iface.get('ifname', '') if name == 'lo' or name.startswith(('veth', 'tap', 'fwbr', 'fwpr', 'fwln')): continue for a in iface.get('addr_info', []): if a.get('family') == 'inet': out.append((name, a['local'])) break return out def pools(): rows = shell('zpool list -H -o name,size,alloc,cap,health').splitlines() return [r.split('\t') for r in rows if r.strip()] def build_menu(): menu = [screen_host, screen_pve, screen_uptime] for name, ip in ipv4_addresses(): menu.append(lambda n=name, i=ip: [n, i]) for name, size, alloc, cap, health in pools(): menu.append(lambda n=name, s=size, a=alloc, c=cap, h=health: [f'{n} {h}', f'{a}/{s} {c}']) return menu # ---------- driver glue ---------- lcd = None lcd_timer = None menu = [screen_host] menu_item = 0 lock = threading.Lock() def render(item): try: lines = menu[item]() except Exception as e: # one broken screen must never kill the menu lines = ['screen error', type(e).__name__] print(f'screen {item} failed: {e!r}') lines = [fit(l) for l in lines][:2] if lcd is None: print(f'[{item}] {lines[0]!r} | {lines[1] if len(lines) > 1 else ""!r}') return lcd.clear() lcd.write(0, lines) def lcd_on(): global lcd_timer lcd.backlight(True) if lcd_timer: lcd_timer.cancel() lcd_timer = threading.Timer(DISPLAY_TIMEOUT, lambda: lcd.backlight(False)) lcd_timer.daemon = True lcd_timer.start() def response_handler(command, data): global menu_item if command != 'Switch_Status': return with lock: lcd_on() if data == 0x01: # up menu_item = (menu_item - 1) % len(menu) elif data == 0x02: # down menu_item = (menu_item + 1) % len(menu) else: return render(menu_item) def main(): global lcd, menu, menu_item if '--dry-run' in sys.argv: menu = build_menu() for i in range(len(menu)): render(i) return sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import qnaplcd lcd = qnaplcd.QnapLCD(PORT, PORT_SPEED, response_handler) lcd.reset() lcd.clear() lcd_on() while True: with lock: menu = build_menu() menu_item %= len(menu) render(menu_item) time.sleep(REFRESH) if __name__ == '__main__': main()