fan-control: read HA mode/manual-% setpoint (HA fan control)
The host daemon now polls input_select.r730_fan_mode (auto/cool/quiet/ manual) + input_number.r730_fan_manual_pct from ha-sofia each loop and routes through fc_resolve: manual holds a fixed %, cool/quiet force that curve, auto keeps the garage-presence behaviour. CEILING still overrides. Ships HA control now on the running host daemon (no Vault); the cluster CronJob migration stays the eventual Terraform home (same logic). HA side (on ha-sofia, auto-git-tracked there): two helpers, an auto- revert-to-auto automation (60min), mode + %-slider control tiles on the dashboard-it Server view. Verified end-to-end: HA manual 70% -> fans 12720rpm; revert to auto -> presence curve 50%. 10 new pure-function tests (fc_resolve/fc_clamp); 46 total green. [ci skip] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b958935ee0
commit
8beca1dfc7
2 changed files with 56 additions and 5 deletions
|
|
@ -37,6 +37,11 @@ set -uo pipefail
|
|||
: "${HA_TOKEN:=}" # long-lived ha-sofia token; empty => presence disabled (COOL only)
|
||||
: "${GARAGE_ENTITY:=sensor.garage_door_state_bg}"
|
||||
: "${GARAGE_OPEN_STATE:=Отворена}" # ha state string meaning "open"
|
||||
# HA control: a mode select + manual % the user drives from Home Assistant.
|
||||
# auto => garage-presence curve (default); cool/quiet => force that curve;
|
||||
# manual => hold MANUAL_ENTITY %. Empty HA_TOKEN or unreachable HA => auto.
|
||||
: "${MODE_ENTITY:=input_select.r730_fan_mode}"
|
||||
: "${MANUAL_ENTITY:=input_number.r730_fan_manual_pct}"
|
||||
: "${PUSHGATEWAY_URL:=}" # optional Prometheus Pushgateway base URL
|
||||
: "${MAX_IPMI_FAILS:=3}"
|
||||
: "${DRY_RUN:=0}" # 1 => log IPMI actions instead of executing
|
||||
|
|
@ -98,6 +103,21 @@ fc_json_str_field() {
|
|||
# fc_pct_to_hex <pct> -> 0xNN
|
||||
fc_pct_to_hex() { printf '0x%02x' "$1"; }
|
||||
|
||||
# fc_clamp <pct> -> 0..100
|
||||
fc_clamp() { local p="$1"; (( p < 0 )) && p=0; (( p > 100 )) && p=100; echo "$p"; }
|
||||
|
||||
# fc_resolve <ha_mode> <temp> <manual_pct> <presence> <current> <deadband> -> pct
|
||||
# HA mode resolution (the hard ceiling is handled by the caller):
|
||||
# manual -> clamp(manual_pct), no hysteresis
|
||||
# cool|quiet -> that curve (with hysteresis)
|
||||
# auto (else) -> presence-driven curve (garage door)
|
||||
fc_resolve() {
|
||||
local ha_mode="$1" temp="$2" manual_pct="$3" presence="$4" current="$5" deadband="$6"
|
||||
if [[ "$ha_mode" == "manual" ]]; then fc_clamp "$manual_pct"; return 0; fi
|
||||
local eff; [[ "$ha_mode" == "auto" ]] && eff="$presence" || eff="$ha_mode"
|
||||
fc_decide "$eff" "$temp" "$current" "$deadband"
|
||||
}
|
||||
|
||||
# ---- side-effecting wrappers ----
|
||||
|
||||
ipmi_manual_on=0
|
||||
|
|
@ -139,9 +159,18 @@ get_presence() {
|
|||
echo "$presence_cache"
|
||||
}
|
||||
|
||||
# ha_entity_state <entity> -> state string (empty if HA disabled/unreachable)
|
||||
ha_entity_state() {
|
||||
[[ -z "$HA_TOKEN" ]] && return 0
|
||||
local resp
|
||||
resp="$(curl -fsS --max-time 5 -H "Authorization: Bearer $HA_TOKEN" \
|
||||
"$HA_URL/api/states/$1" 2>/dev/null)" || return 0
|
||||
fc_json_str_field "$resp" state
|
||||
}
|
||||
|
||||
push_metrics() { # <temp> <pct> <mode> <ha_ok> <fallback>
|
||||
[[ -z "$PUSHGATEWAY_URL" ]] && return 0
|
||||
local mode_num; case "$3" in quiet) mode_num=1;; cool) mode_num=2;; *) mode_num=0;; esac
|
||||
local mode_num; case "$3" in quiet) mode_num=1;; cool) mode_num=2;; manual) mode_num=3;; *) mode_num=0;; esac
|
||||
curl -fsS --max-time 5 --data-binary @- \
|
||||
"$PUSHGATEWAY_URL/metrics/job/fan_control/instance/pve-r730" >/dev/null 2>&1 <<EOF || true
|
||||
# TYPE pve_fan_control_cpu_temp_celsius gauge
|
||||
|
|
@ -189,13 +218,23 @@ main() {
|
|||
fi
|
||||
fi
|
||||
|
||||
local mode ha_ok=1; mode="$(get_presence)"; [[ -z "$HA_TOKEN" ]] && ha_ok=0
|
||||
local pct; pct="$(fc_decide "$mode" "$temp" "$current" "$DEADBAND")"
|
||||
# HA-desired mode (auto/cool/quiet/manual); unreachable/unset => auto.
|
||||
local ha_mode ha_ok=1; ha_mode="$(ha_entity_state "$MODE_ENTITY")"; [[ -z "$HA_TOKEN" ]] && ha_ok=0
|
||||
[[ -z "$ha_mode" ]] && ha_mode="auto"
|
||||
case "$ha_mode" in auto|cool|quiet|manual) ;; *) ha_mode="auto" ;; esac
|
||||
local manual_pct=0
|
||||
if [[ "$ha_mode" == "manual" ]]; then
|
||||
manual_pct="$(ha_entity_state "$MANUAL_ENTITY")"; manual_pct="${manual_pct%%.*}"
|
||||
[[ "$manual_pct" =~ ^[0-9]+$ ]] || manual_pct=0
|
||||
fi
|
||||
local presence="cool"; [[ "$ha_mode" == "auto" ]] && presence="$(get_presence)"
|
||||
local eff; if [[ "$ha_mode" == "manual" ]]; then eff="manual"; elif [[ "$ha_mode" == "auto" ]]; then eff="$presence"; else eff="$ha_mode"; fi
|
||||
local pct; pct="$(fc_resolve "$ha_mode" "$temp" "$manual_pct" "$presence" "$current" "$DEADBAND")"
|
||||
if (( pct != current )); then
|
||||
if set_manual "$pct"; then log "temp=${temp}C mode=${mode} fan=${pct}% (was ${current}%)"; current="$pct"
|
||||
if set_manual "$pct"; then log "temp=${temp}C ha_mode=${ha_mode} eff=${eff} fan=${pct}% (was ${current}%)"; current="$pct"
|
||||
else log "WARN set_manual ${pct}% failed"; fi
|
||||
fi
|
||||
push_metrics "$temp" "$current" "$mode" "$ha_ok" 0
|
||||
push_metrics "$temp" "$current" "$eff" "$ha_ok" 0
|
||||
(( RUN_ONCE == 1 )) && break || sleep "$LOOP_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@ eq "decide down past band" 60 "$(fc_decide cool 69 80 3)" # 69+3=72 -> 60%
|
|||
eq "decide 100 holds" 100 "$(fc_decide cool 77 100 3)" # 77+3=80 -> 100 -> hold
|
||||
eq "decide 100 drops" 80 "$(fc_decide cool 75 100 3)" # 75+3=78 -> 80 < 100 -> drop
|
||||
|
||||
# --- fc_clamp / fc_resolve: HA mode resolution ---
|
||||
eq "clamp over 100" 100 "$(fc_clamp 150)"
|
||||
eq "clamp under 0" 0 "$(fc_clamp -5)"
|
||||
eq "clamp passthrough" 45 "$(fc_clamp 45)"
|
||||
eq "resolve manual=slider" 42 "$(fc_resolve manual 64 42 cool -1 3)"
|
||||
eq "resolve manual clamped" 100 "$(fc_resolve manual 64 150 cool -1 3)"
|
||||
eq "resolve cool=cool curve" 60 "$(fc_resolve cool 64 0 cool -1 3)"
|
||||
eq "resolve quiet=quiet curve" 65 "$(fc_resolve quiet 80 0 cool -1 3)"
|
||||
eq "resolve auto+empty=cool" 60 "$(fc_resolve auto 64 0 cool -1 3)"
|
||||
eq "resolve auto+present=quiet" 20 "$(fc_resolve auto 64 0 quiet -1 3)"
|
||||
eq "resolve cool hysteresis" 60 "$(fc_resolve cool 69 0 cool 80 3)"
|
||||
|
||||
# --- presence ---
|
||||
now=1000000
|
||||
eq "presence open -> quiet" quiet "$(fc_presence_mode Отворена 0 $now 900 Отворена)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue