#!/bin/sh

###########################################################################
#
# fancontrol.sh
#
# Dell PowerEdge Fan Controller
# Version : 2.0
# Platform : TrueNAS
#
# Description:
#   Dynamic fan controller for Dell PowerEdge servers using local IPMI.
#   Designed to run as a background daemon launched by
#   start-fancontrol.sh.
#
# Tested on:
#   - Dell PowerEdge R730xd
#   - iDRAC8
#   - Dell HBA330
#   - TrueNAS
#
# Features:
#   - Dynamic fan control
#   - CPU temperature monitoring
#   - Inlet temperature monitoring
#   - Exhaust temperature monitoring
#   - Automatic fallback to Dell Auto
#   - Sensor retry protection
#
# Planned Features:
#   - HDD SMART temperature monitoring
#   - Hysteresis
#   - Automatic disk discovery
#   - Configuration file support
#
# License : MIT
#
# Author : Jack Bamford
# Date   : July 2026
#
###########################################################################

FAN_NORMAL="0x1C"    # 28%
FAN_WARM="0x20"      # 32%
FAN_HOT="0x28"       # 40%

CPU_WARM=55
CPU_HOT=65
CPU_AUTO=75

INLET_WARM=30
INLET_HOT=35

EXHAUST_AUTO=50

CHECK_INTERVAL=60

MAX_FAILURES=3
FAILURES=0

CURRENT=""

log()
{
    logger "R730xd Fan Controller: $1"
}

set_manual()
{
    ipmitool raw 0x30 0x30 0x01 0x00 >/dev/null 2>&1
}

set_auto()
{
    ipmitool raw 0x30 0x30 0x01 0x01 >/dev/null 2>&1
}

set_fans()
{
    set_manual &&
    ipmitool raw 0x30 0x30 0x02 0xff "$1" >/dev/null 2>&1
}

get_temps()
{
    SDR="$(ipmitool sdr list 2>/dev/null)"

    INLET="$(echo "$SDR" | awk -F'|' '
        /Inlet Temp/ {
            gsub(/ /, "", $2)
            sub(/degreesC/, "", $2)
            print int($2)
        }
    ')"

    EXHAUST="$(echo "$SDR" | awk -F'|' '
        /Exhaust Temp/ {
            gsub(/ /, "", $2)
            sub(/degreesC/, "", $2)
            print int($2)
        }
    ')"

    CPU_MAX="$(echo "$SDR" | awk -F'|' '
        /^Temp[[:space:]]*\|/ {
            gsub(/ /, "", $2)
            sub(/degreesC/, "", $2)

            value = int($2)

            if (value > max)
                max = value
        }

        END {
            if (max > 0)
                print max
        }
    ')"
}

cleanup()
{
    log "Stopping - returning fan control to Dell Auto"
    set_auto
    exit 0
}

trap cleanup INT TERM HUP

log "Starting"

until ipmitool mc info >/dev/null 2>&1
do
    sleep 2
done

log "IPMI ready"

# Set fans immediately after startup.
if ! set_fans "$FAN_NORMAL"; then
    log "Unable to set initial fan speed - returning to Dell Auto"
    set_auto
    exit 1
fi

CURRENT="$FAN_NORMAL"
log "Initial fan speed set to $FAN_NORMAL"

while true
do
    get_temps

    if [ -z "$CPU_MAX" ] || [ -z "$INLET" ] || [ -z "$EXHAUST" ]; then
        FAILURES=$((FAILURES + 1))

        log "Sensor read failed (${FAILURES}/${MAX_FAILURES}) CPU='${CPU_MAX}' INLET='${INLET}' EXHAUST='${EXHAUST}'"

        if [ "$FAILURES" -ge "$MAX_FAILURES" ]; then
            log "Too many consecutive sensor failures - returning control to Dell Auto"
            set_auto
            exit 1
        fi

        sleep 2
        continue
    fi

    # Successful sensor read.
    FAILURES=0

    if [ "$CPU_MAX" -ge "$CPU_AUTO" ] || \
       [ "$EXHAUST" -ge "$EXHAUST_AUTO" ]; then

        NEW="AUTO"

    elif [ "$CPU_MAX" -ge "$CPU_HOT" ] || \
         [ "$INLET" -ge "$INLET_HOT" ]; then

        NEW="$FAN_HOT"

    elif [ "$CPU_MAX" -ge "$CPU_WARM" ] || \
         [ "$INLET" -ge "$INLET_WARM" ]; then

        NEW="$FAN_WARM"

    else
        NEW="$FAN_NORMAL"
    fi

    if [ "$NEW" != "$CURRENT" ]; then
        log "CPU=${CPU_MAX}C Inlet=${INLET}C Exhaust=${EXHAUST}C Fan=${NEW}"

        if [ "$NEW" = "AUTO" ]; then
            set_auto
        else
            set_fans "$NEW"
        fi

        CURRENT="$NEW"
    fi

    sleep "$CHECK_INTERVAL"
done
