feat: add standalone capability-based cooling plugin
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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,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 uses presets: 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}
|
||||
Reference in New Issue
Block a user