99 lines
7.2 KiB
Python
99 lines
7.2 KiB
Python
"""Configuration and public capability schema; no hardware imports."""
|
|
import configparser
|
|
import math
|
|
from pathlib import Path
|
|
|
|
VERSION = "1.0.1"
|
|
PLUGIN_ID = "yahboom-cooling-hat"
|
|
COLORS = {"red":0,"green":1,"blue":2,"yellow":3,"purple":4,"cyan":5,"white":6}
|
|
EFFECTS = {"flow":0,"breathe":1,"marquee":2,"rainbow":3,"colorful":4}
|
|
GROUPS = {"cpu":"red","power":"yellow","memory":"purple","storage":"white","network":"blue","service":"cyan"}
|
|
DEFAULT_RGB = {"cpu":(255,0,0),"power":(255,40,0),"memory":(255,0,255),"storage":(255,255,255),"network":(0,0,255),"service":(0,255,96),"normal":(0,0,0)}
|
|
SCHEMA = {}
|
|
|
|
def option(section,key,default,zh,en,kind="number",minimum=None,maximum=None,choices=None,unit="",visible=None):
|
|
SCHEMA.setdefault(section,{})[key] = dict(default=default,label={"zh":zh,"en":en},type=kind,
|
|
min=minimum,max=maximum,choices=choices,unit=unit,visible=visible)
|
|
|
|
for key,value,label in (("start",50,"启动温度"),("level2",53,"二档温度"),("level3",56,"三档温度"),("level4",59,"四档温度"),("max",62,"全速温度"),("hysteresis",1.5,"降档回差")):
|
|
option("fan",key,value,label,key.replace('_',' '),minimum=0,maximum=100 if key!="hysteresis" else 20,unit="°C")
|
|
option("integration","enabled","false","允许监控接入(重启生效)","Allow monitor integration (restart required)","enum",choices=["false","true"])
|
|
option("oled","refresh_seconds",1,"刷新周期","Refresh interval",minimum=.2,maximum=10,unit="s")
|
|
option("oled","page_seconds",10,"轮播周期","Page interval",minimum=1,maximum=120,unit="s")
|
|
option("rgb","alert_mode","breathe","告警模式","Alert mode","enum",choices=["breathe","flash"])
|
|
option("rgb","normal_mode","off","正常状态灯效","Normal lighting","enum",choices=["off","solid","flash",*EFFECTS])
|
|
option("rgb","normal_effect_color","green","正常灯效颜色","Normal effect color","enum",choices=list(COLORS),visible={"normal_mode":["flow","breathe"]})
|
|
option("rgb","normal_effect_speed",2,"正常灯效速度档","Normal effect speed",minimum=1,maximum=3,visible={"normal_mode":list(EFFECTS)})
|
|
option("rgb","custom_min_hold_ms",1500,"自定义闪烁最小保持","Minimum custom flash hold",minimum=1500,maximum=60000,unit="ms")
|
|
option("rgb","write_delay_ms",10,"内置效果指令间隔","Built-in effect command gap",minimum=1,maximum=1000,unit="ms")
|
|
for level,value in (("warning",1),("critical",2),("emergency",3)):
|
|
option("rgb",level+"_speed",value,{"warning":"普通","critical":"严重","emergency":"紧急"}[level]+"呼吸速度档",level.title()+" breathing speed",minimum=1,maximum=3,visible={"alert_mode":["breathe"]})
|
|
for edge in ("on","off"):
|
|
option("rgb",f"{level}_flash_{edge}_ms",1500,f"{ {'warning':'普通','critical':'严重','emergency':'紧急'}[level]}告警{'亮灯' if edge=='on' else '灭灯'}时间",f"{level.title()} flash {edge}",minimum=50,maximum=60000,unit="ms",visible={"alert_mode":["flash"]})
|
|
for edge in ("on","off"):
|
|
option("rgb",f"normal_flash_{edge}_ms",1500,"正常闪烁"+("亮灯" if edge=="on" else "灭灯")+"时间",f"Normal flash {edge}",minimum=50,maximum=60000,unit="ms",visible={"normal_mode":["flash"]})
|
|
for group,color in GROUPS.items():
|
|
title={"cpu":"处理器、温度与散热","power":"电源","memory":"内存","storage":"存储","network":"网络","service":"服务与进程"}[group]
|
|
option("rgb",group+"_breathe_color",color,title+"呼吸颜色",group.title()+" breathing color","enum",choices=list(COLORS),visible={"alert_mode":["breathe"]})
|
|
for group,values in DEFAULT_RGB.items():
|
|
for channel,value in zip("rgb",values):
|
|
title={"cpu":"处理器","power":"电源","memory":"内存","storage":"存储","network":"网络","service":"服务","normal":"正常"}[group]
|
|
option("rgb",f"{group}_{channel}",value,title+" · "+channel.upper(),group.title()+" · "+channel.upper(),minimum=0,maximum=255,
|
|
visible={"normal_mode":["solid","flash"]} if group=="normal" else {"alert_mode":["flash"]})
|
|
|
|
for group in DEFAULT_RGB:
|
|
for channel in "rgb":
|
|
SCHEMA["rgb"][group+"_"+channel].update(color_group=group,channel=channel)
|
|
|
|
def validated(data):
|
|
if not isinstance(data,dict):raise ValueError("configuration object required")
|
|
result={}
|
|
for section,values in data.items():
|
|
if section not in SCHEMA or not isinstance(values,dict): raise ValueError("unknown configuration section")
|
|
result[section]={}
|
|
for key,value in values.items():
|
|
if key not in SCHEMA[section]: raise ValueError("unknown option: "+key)
|
|
spec=SCHEMA[section][key]
|
|
if spec["type"]=="enum":
|
|
if value not in spec["choices"]: raise ValueError("invalid choice: "+key)
|
|
else:
|
|
if isinstance(value,bool): raise ValueError("invalid number: "+key)
|
|
value=float(value)
|
|
if not math.isfinite(value) or not spec["min"]<=value<=spec["max"]: raise ValueError("out of range: "+key)
|
|
if section=="rgb":
|
|
if not value.is_integer(): raise ValueError("integer required: "+key)
|
|
value=int(value)
|
|
result[section][key]=value
|
|
return result
|
|
|
|
def load(path):
|
|
result={s:{k:v["default"] for k,v in options.items()} for s,options in SCHEMA.items()}
|
|
cfg=configparser.ConfigParser();cfg.read(path)
|
|
raw={s:dict(cfg.items(s)) for s in cfg.sections()}
|
|
for section,values in validated(raw).items():result[section].update(values)
|
|
check(result)
|
|
return result
|
|
|
|
def check(config):
|
|
curve=[config["fan"][k] for k in ("start","level2","level3","level4","max")]
|
|
if any(a>=b for a,b in zip(curve,curve[1:])):raise ValueError("fan thresholds must strictly increase")
|
|
|
|
def save(path,config):
|
|
check(config)
|
|
parser=configparser.ConfigParser();parser.read_dict(config)
|
|
path=Path(path); temporary=path.with_suffix('.tmp')
|
|
with temporary.open('w') as f:parser.write(f)
|
|
temporary.chmod(0o640);temporary.replace(path)
|
|
|
|
def descriptor():
|
|
return {"api_version":1,"id":PLUGIN_ID,"name":"Yahboom RGB Cooling HAT","version":VERSION,
|
|
"capabilities":{
|
|
"fan":{"control":"levels","levels":["OFF","L1","L2","L3","L4","MAX"],"rpm":False,"autonomous":True,"owner":"plugin"},
|
|
"display":{"width":128,"height":32,"pages":True},
|
|
"rgb":{"modes":list(dict.fromkeys(["off","breathe","flash","solid",*EFFECTS])),"preset_colors":list(COLORS),"custom_flash_experimental":True}},
|
|
"notes":[{"zh":"内置呼吸可按告警对象选择预置颜色(红、绿、蓝、黄、紫、青、白);默认CPU红、电源黄、内存紫、存储白、网络蓝、服务青。自定义闪烁仍可能异色。",
|
|
"en":"Built-in breathing colors are configurable per alert category (red, green, blue, yellow, purple, cyan, white). Defaults: CPU red, power yellow, memory purple, storage white, network blue, services cyan. Custom flashing may still show incorrect colors."},
|
|
{"zh":"自定义闪烁的实际亮灭保持时间不低于最小保持时间。停止插件会请求风扇全速;风扇没有RPM反馈。",
|
|
"en":"Custom flash phases last at least the minimum hold time. Stopping the plugin requests full fan speed; RPM feedback is unavailable."}],
|
|
"schema":SCHEMA}
|