"""Configuration and public capability schema; no hardware imports.""" import configparser import math from pathlib import Path VERSION = "1.0.0" 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("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}