From 5977eba50325ee4fd26794278c3e25f299cf831c Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 27 Aug 2026 12:00:43 -0500 Subject: [PATCH] feat(sensors): sysfs thermal fallback + SBC fleet coverage [#341][#457][#458] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lm-sensors is silent on SBCs (Pis, Jetson-class), so the wrapper now falls back to /sys/class/thermal zones rendered in lm-sensors format when native sensors output is empty; suppressed when native output exists to avoid duplicates. TDD: 3 new unit tests (12 green). Sensor stack deployed to jetson + pfvsvrpi + 3 subopis (verified from poller), HA pack extended to 16 sensors across 14 hosts, high-temp automation covers the SBC fleet. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- dcinfra/ha/automations-ups-alerts.yaml | 5 ++ dcinfra/ha/sensors-snmp-plant.yaml | 76 ++++++++++++++++++++++ dcinfra/sensors/temper/lmsensors-extend.sh | 41 +++++++++++- tests/unit/lmsensors.sh | 59 ++++++++++++++++- 4 files changed, 178 insertions(+), 3 deletions(-) diff --git a/dcinfra/ha/automations-ups-alerts.yaml b/dcinfra/ha/automations-ups-alerts.yaml index aa1d1ab..d9a0ed1 100644 --- a/dcinfra/ha/automations-ups-alerts.yaml +++ b/dcinfra/ha/automations-ups-alerts.yaml @@ -120,6 +120,11 @@ - sensor.pfv_tsys6_cpu_temperature - sensor.pfv_tsys7_cpu_temperature - sensor.pfv_tsys9_cpu_temperature + - sensor.pfv_jetson_temperature + - sensor.pfv_svrpi_temperature + - sensor.pfv_subopi3_temperature + - sensor.pfv_subopi_dev_3_temperature + - sensor.pfv_subopi_dev_4_temperature above: 104 actions: - action: notify.send_message diff --git a/dcinfra/ha/sensors-snmp-plant.yaml b/dcinfra/ha/sensors-snmp-plant.yaml index 4788e6f..9fec4ae 100644 --- a/dcinfra/ha/sensors-snmp-plant.yaml +++ b/dcinfra/ha/sensors-snmp-plant.yaml @@ -197,6 +197,82 @@ sensor: {% set m = value | regex_findall('Package id 0:\s*\+?(-?\d+\.?\d*)') %} {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + # --- SBC fleet (sysfs thermal fallback in lmsensors-extend v2; temp1 = zone0/SoC) --- + - platform: snmp + name: "PFV jetson temperature" + host: pfv-jetson-nano-1.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_jetson_temperature + value_template: >- + {% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + - platform: snmp + name: "PFV svrpi temperature" + host: pfvsvrpi.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_svrpi_temperature + value_template: >- + {% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + - platform: snmp + name: "PFV subopi3 temperature" + host: subopi3.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_subopi3_temperature + value_template: >- + {% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + - platform: snmp + name: "PFV subopi-dev-3 temperature" + host: subopi-dev-3.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_subopi_dev_3_temperature + value_template: >- + {% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + - platform: snmp + name: "PFV subopi-dev-4 temperature" + host: subopi-dev-4.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_subopi_dev_4_temperature + value_template: >- + {% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + # --------------------------------------------------------------------------- # PLACEHOLDERS — enable after dependencies land (do not invent OIDs live): # diff --git a/dcinfra/sensors/temper/lmsensors-extend.sh b/dcinfra/sensors/temper/lmsensors-extend.sh index 7eb2686..6e98156 100644 --- a/dcinfra/sensors/temper/lmsensors-extend.sh +++ b/dcinfra/sensors/temper/lmsensors-extend.sh @@ -14,6 +14,7 @@ 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. @@ -71,10 +72,41 @@ run_native_sensors() { "$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" +} + main() { - local out="" block internal external native + 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)" \ @@ -85,8 +117,13 @@ main() { 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 [[ -n "$out" ]] || { - echo "error: no sensor sources available (lm-sensors pkg? TEMPer venv?)" >&2 + echo "error: no sensor sources available (lm-sensors pkg? TEMPer venv? sysfs thermal?)" >&2 return 1 } printf '%s' "$out" diff --git a/tests/unit/lmsensors.sh b/tests/unit/lmsensors.sh index 8c811ff..afe5c73 100644 --- a/tests/unit/lmsensors.sh +++ b/tests/unit/lmsensors.sh @@ -131,7 +131,7 @@ test_main_merge() { test_main_neither_fails() { make_stub_lsusb noprobe if PATH="$STUB_DIR:$PATH" NATIVE_SENSORS_BIN=/nonexistent \ - VENV_PY=/nonexistent main >/dev/null 2>&1; then + VENV_PY=/nonexistent SYSFS_THERMAL_ROOT=/nonexistent main >/dev/null 2>&1; then echo "❌ main: succeeded with no sensor sources" ((++failed)) else @@ -139,6 +139,60 @@ test_main_neither_fails() { fi } +# sysfs thermal-zone fallback (SBCs: Pis, Jetson-class, /sys/class/thermal) +make_sysfs_fixture() { + SYSFS_FIXTURE="$STUB_DIR/sysfs/class-thermal" + mkdir -p "$SYSFS_FIXTURE/thermal_zone0" "$SYSFS_FIXTURE/thermal_zone1" + printf 'cpu-thermal\n' >"$SYSFS_FIXTURE/thermal_zone0/type" + printf '45200\n' >"$SYSFS_FIXTURE/thermal_zone0/temp" + printf 'gpu-thermal\n' >"$SYSFS_FIXTURE/thermal_zone1/type" + printf '52350\n' >"$SYSFS_FIXTURE/thermal_zone1/temp" +} + +test_format_sysfs_block() { + local got expected + expected=$(printf 'soc-thermal-virtual-0\nAdapter: Virtual device\ntemp1: +45.2°C\ntemp2: +52.4°C') + got="$(printf 'cpu-thermal 45200\ngpu-thermal 52350\n' | format_sysfs_block "$(printf 'cpu-thermal 45200\ngpu-thermal 52350\n')")" + if [[ "$got" == "$expected" ]]; then + echo "✅ format_sysfs_block renders lm-sensors format from millicelsius" + else + echo "❌ format_sysfs_block output mismatch" + printf 'got:\n%s\nexpected:\n%s\n' "$got" "$expected" + ((++failed)) + fi +} + +test_main_sysfs_fallback() { + make_stub_lsusb noprobe + make_sysfs_fixture + local got + got="$(PATH="$STUB_DIR:$PATH" NATIVE_SENSORS_BIN=/nonexistent VENV_PY=/nonexistent \ + SYSFS_THERMAL_ROOT="$SYSFS_FIXTURE" main)" + if grep -q 'temp1: *+45.2°C' <<<"$got" && grep -q 'soc-thermal-virtual-0' <<<"$got"; then + echo "✅ main: sysfs fallback when lm-sensors silent (SBC)" + else + echo "❌ main: sysfs fallback composition wrong" + printf 'got:\n%s\n' "$got" + ((++failed)) + fi +} + +test_main_native_suppresses_sysfs() { + make_stub_lsusb noprobe + make_stub_sensors + make_sysfs_fixture + local got + got="$(PATH="$STUB_DIR:$PATH" NATIVE_SENSORS_BIN="$STUB_DIR/sensors" VENV_PY=/nonexistent \ + SYSFS_THERMAL_ROOT="$SYSFS_FIXTURE" main)" + if grep -q 'Package id 0' <<<"$got" && ! grep -q 'soc-thermal-virtual-0' <<<"$got"; then + echo "✅ main: native output present, sysfs fallback suppressed (no duplicates)" + else + echo "❌ main: sysfs fallback leaked alongside native output" + printf 'got:\n%s\n' "$got" + ((++failed)) + fi +} + test_format_two_sensors test_format_single_sensor test_format_negative @@ -147,6 +201,9 @@ test_main_native_only test_main_temper_only test_main_merge test_main_neither_fails +test_format_sysfs_block +test_main_sysfs_fallback +test_main_native_suppresses_sysfs if ((failed > 0)); then echo "❌ lm-sensors extend: $failed test(s) failed"