#!/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 " " 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