163 lines
4.8 KiB
Python
163 lines
4.8 KiB
Python
#!/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()
|