2026-09-27 20:24:15 +08:00
"""Configuration and public capability schema; no hardware imports."""
import configparser
import math
from pathlib import Path
2026-09-27 23:52:27 +08:00
VERSION = "1.0.1"
2026-09-27 20:24:15 +08:00
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" )
2026-09-27 22:28:05 +08:00
option ( "integration" , "enabled" , "false" , "允许监控接入(重启生效)" , "Allow monitor integration (restart required)" , "enum" , choices = [ "false" , "true" ])
2026-09-27 20:24:15 +08:00
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" ]})
2026-09-27 21:14:22 +08:00
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" ]})
2026-09-27 20:24:15 +08:00
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 }},
2026-09-27 21:14:22 +08:00
"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." },
2026-09-27 20:24:15 +08:00
{ "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 }