Hosts opt in via /etc/snmp/lmsensors-zone-types (one zone type per line, e.g. Jetson GPU-therm); each type renders its own zone-<type> chip block so HA templates anchor on the chip name. Default behavior unchanged; no snmpd restarts (extend re-execs per poll).
197 lines
7.3 KiB
Bash
197 lines
7.3 KiB
Bash
#!/bin/bash
|
|
# lm-sensors extend for snmpd: native sensors + TEMPer USB block [#341/#439]
|
|
#
|
|
# Deployed to /usr/local/bin/lmsensors-extend on sensor hosts by deploy.sh.
|
|
# Output = lm-sensors format, consumed by Home Assistant's SNMP integration
|
|
# (nsExtendOutputFull."lmsensors") and, if enabled later, LibreNMS's
|
|
# lm-sensors app.
|
|
#
|
|
# Layers (each optional, at least one required):
|
|
# 1. native /usr/bin/sensors output (CPU/board temps, fans) — lm-sensors pkg
|
|
# 2. TEMPer USB probe (temperusb in venv /opt/temper-venv) — probe-optional
|
|
# (venv-on-host is founder-approved for sensor hosts, incl. Proxmox)
|
|
set -euo pipefail
|
|
|
|
VENV_PY="${VENV_PY:-/opt/temper-venv/bin/python}"
|
|
NATIVE_SENSORS_BIN="${NATIVE_SENSORS_BIN:-/usr/bin/sensors}"
|
|
SYSFS_THERMAL_ROOT="${SYSFS_THERMAL_ROOT:-/sys/class/thermal}"
|
|
CHIP_NAME="${CHIP_NAME:-temper-usb-1}"
|
|
|
|
# Render lm-sensors format block from internal/external Celsius values.
|
|
# Empty/absent external renders internal only. Non-numeric input is rejected.
|
|
format_block() {
|
|
local internal="${1:-}" external="${2:-}"
|
|
local num_re='^-?[0-9]+(\.[0-9]+)?$'
|
|
[[ "$internal" =~ $num_re ]] || {
|
|
echo "error: internal temp not numeric: '$internal'" >&2
|
|
return 1
|
|
}
|
|
local signed
|
|
printf '%s\nAdapter: USB adapter\n' "$CHIP_NAME"
|
|
signed="${internal#+}"; [[ "$signed" == -* ]] || signed="+$signed"
|
|
printf 'Internal: %s°C\n' "$signed"
|
|
if [[ -n "$external" && "$external" =~ $num_re ]]; then
|
|
signed="${external#+}"; [[ "$signed" == -* ]] || signed="+$signed"
|
|
printf 'External: %s°C\n' "$signed"
|
|
fi
|
|
}
|
|
|
|
# True when a TEMPer USB dongle (0c45:7401) is attached and the venv exists.
|
|
|
|
# Numeric mode: print ONE Celsius float for snmpd extend lmsensors_n [#618].
|
|
# Preference: TEMPer external > TEMPer internal > first native/sysfs temp
|
|
# reading. Consumed by HA's SNMP config-entry integration (device_class
|
|
# temperature; HA renders C in the instance display unit).
|
|
numeric_pick() {
|
|
local internal="${1:-}" external="${2:-}" native="${3:-}" pick
|
|
local num_re='^-?[0-9]+(\.[0-9]+)?$'
|
|
for pick in "$external" "$internal"; do
|
|
pick="${pick#+}"
|
|
[[ "$pick" =~ $num_re ]] && { printf '%s\n' "$pick"; return 0; }
|
|
done
|
|
if [[ -n "$native" ]]; then
|
|
pick="$(grep -oE "\-?[0-9]+(\.[0-9]+)?°C" <<<"$native" | head -1)"; pick="${pick%°C}"
|
|
[[ "$pick" =~ $num_re ]] && { printf '%s\n' "$pick"; return 0; }
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
numeric_main() {
|
|
local internal external line
|
|
if has_temper_probe; then
|
|
if read -r internal external <<<"$(read_temper 2>/dev/null)"; then
|
|
numeric_pick "${internal:-}" "${external:-}" "" && return 0
|
|
fi
|
|
fi
|
|
line="$(run_native_sensors | grep -m1 '°C')"
|
|
[[ -n "$line" ]] && numeric_pick "" "" "$line" && return 0
|
|
line="$(read_sysfs_thermal 2>/dev/null | head -1 | awk '{printf "+%.1f°C", $2/1000}')"
|
|
[[ -n "$line" ]] && numeric_pick "" "" "$line" && return 0
|
|
echo "error: numeric mode found no sensor source" >&2
|
|
return 1
|
|
}
|
|
has_temper_probe() {
|
|
command -v lsusb >/dev/null 2>&1 || return 1
|
|
[[ -x "$VENV_PY" ]] || return 1
|
|
lsusb | grep -qi '0c45:7401'
|
|
}
|
|
|
|
# Read the TEMPer probe via temperusb; prints "<internal> <external>" in C.
|
|
# Sensor 0 = internal, sensor 1 = external probe (if attached).
|
|
read_temper() {
|
|
"$VENV_PY" - <<'PY'
|
|
from temperusb import TemperHandler
|
|
|
|
internal = external = ""
|
|
for device in TemperHandler().get_devices():
|
|
try:
|
|
temps = device.get_temperatures(sensors=[0, 1])
|
|
except (ValueError, KeyError):
|
|
temps = device.get_temperatures()
|
|
for index, slot in ((0, "internal"), (1, "external")):
|
|
reading = temps.get(index)
|
|
if reading and reading.get("temperature_c") is not None:
|
|
value = f"{reading['temperature_c']:.1f}"
|
|
if slot == "internal":
|
|
internal = value
|
|
else:
|
|
external = value
|
|
print(f"{internal} {external}".strip())
|
|
PY
|
|
}
|
|
|
|
run_native_sensors() {
|
|
[[ -x "$NATIVE_SENSORS_BIN" ]] || return 1
|
|
"$NATIVE_SENSORS_BIN" 2>/dev/null
|
|
}
|
|
|
|
# SBC fallback (Raspberry Pi et al.): lm-sensors often reports nothing; SoC
|
|
# temperatures live in /sys/class/thermal/thermal_zone*/{type,temp} (mC).
|
|
read_sysfs_thermal() {
|
|
[[ -d "$SYSFS_THERMAL_ROOT" ]] || return 1
|
|
local z type mc found=0
|
|
for z in "$SYSFS_THERMAL_ROOT"/thermal_zone[0-9]*; do
|
|
[[ -r "$z/type" && -r "$z/temp" ]] || continue
|
|
type="$(<"$z/type")" mc="$(<"$z/temp")"
|
|
[[ "$mc" =~ ^-?[0-9]+$ ]] || continue
|
|
printf '%s %s\n' "$type" "$mc"
|
|
found=1
|
|
done
|
|
((found)) || return 1
|
|
}
|
|
|
|
# Render lm-sensors format block from read_sysfs_thermal lines "type millicelsius".
|
|
format_sysfs_block() {
|
|
local input="${1:-}" type mc signed n=0
|
|
[[ -n "$input" ]] || return 1
|
|
printf 'soc-thermal-virtual-0\nAdapter: Virtual device\n'
|
|
while read -r type mc; do
|
|
((++n))
|
|
signed="$(awk -v mc="$mc" 'BEGIN { printf "%.1f", mc / 1000 }')"
|
|
signed="${signed#+}"
|
|
[[ "$signed" == -* ]] || signed="+$signed"
|
|
printf 'temp%d: %s°C\n' "$n" "$signed"
|
|
done <<<"$input"
|
|
}
|
|
|
|
# Labeled per-zone temps (e.g. Jetson GPU-therm) [#736]. Hosts opt in by
|
|
# listing zone types (one per line) in /etc/snmp/lmsensors-zone-types; no
|
|
# snmpd.conf change or restart needed (extend re-execs per poll). Each type
|
|
# renders its own chip block so HA templates can anchor on the chip name.
|
|
ZONE_TYPES_FILE="${ZONE_TYPES_FILE:-/etc/snmp/lmsensors-zone-types}"
|
|
|
|
format_labeled_zones() {
|
|
local z type mc want signed
|
|
[[ -r "$ZONE_TYPES_FILE" ]] || return 1
|
|
while IFS= read -r want; do
|
|
want="${want%%#*}"; want="${want//[[:space:]]/}"
|
|
[[ -n "$want" ]] || continue
|
|
for z in "$SYSFS_THERMAL_ROOT"/thermal_zone[0-9]*; do
|
|
[[ -r "$z/type" && -r "$z/temp" ]] || continue
|
|
[[ "$(<"$z/type")" == "$want" ]] || continue
|
|
mc="$(<"$z/temp")"
|
|
[[ "$mc" =~ ^-?[0-9]+$ ]] || continue
|
|
signed="$(awk -v mc="$mc" 'BEGIN { printf "%.1f", mc / 1000 }')"
|
|
signed="${signed#+}"
|
|
[[ "$signed" == -* ]] || signed="+$signed"
|
|
printf 'zone-%s-virtual-0\nAdapter: Virtual device\ntemp1: %s°C\n\n' "$want" "$signed"
|
|
done
|
|
done <"$ZONE_TYPES_FILE"
|
|
return 0
|
|
}
|
|
|
|
main() {
|
|
local out="" block internal external native="" sysinput
|
|
if native="$(run_native_sensors)" && [[ -n "$native" ]]; then
|
|
out+="$native"$'\n'
|
|
else
|
|
native=""
|
|
fi
|
|
if has_temper_probe; then
|
|
if block="$(read_temper 2>/dev/null)" \
|
|
&& read -r internal external <<<"$block" \
|
|
&& block="$(format_block "${internal:-}" "${external:-}")"; then
|
|
out+="$block"$'\n'
|
|
else
|
|
echo "warn: TEMPer probe present but read failed" >&2
|
|
fi
|
|
fi
|
|
if [[ -z "$native" ]] \
|
|
&& sysinput="$(read_sysfs_thermal 2>/dev/null)" \
|
|
&& block="$(format_sysfs_block "$sysinput")"; then
|
|
out+="$block"$'\n'
|
|
fi
|
|
if block="$(format_labeled_zones)" && [[ -n "$block" ]]; then
|
|
out+="$block"$'\n'
|
|
fi
|
|
[[ -n "$out" ]] || {
|
|
echo "error: no sensor sources available (lm-sensors pkg? TEMPer venv? sysfs thermal?)" >&2
|
|
return 1
|
|
}
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
if [[ "${1:-}" == "-n" ]]; then numeric_main; else main; fi
|
|
fi
|