Files
pigway-yahboom-cooling-hat/app/settings.py
T

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.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("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}