feat(host_agent): a temperature sensor is judged by how it moves against the CPU (#5783)
A board's spare thermistor inputs report whatever an open input reads, and the value rule from #5759 only catches the impossible ones. Once an hour, sensors.refresh judges each sensor against the host's CPU (Package id N, Tctl/Tdie/TccdN, PECI Agent N) over a week of hourly averages. A sensor is ruled out if it falls as the CPU warms (r <= -0.5) or holds within 0.5 °C while the CPU moved 10 °C or more. A judgment needs at least 72 shared hours. The host temperature leaves judged sensors out at ingest. The host page lists them under "Not counted", with each reason given once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,15 +113,28 @@ def counts_as_hottest(label: str, celsius: float) -> bool:
|
||||
return not (_BOARD_INPUT.match(label) and celsius >= BOARD_INPUT_CEILING_C)
|
||||
|
||||
|
||||
def split_temps(temps: dict) -> tuple[dict, dict]:
|
||||
def is_thermometer(host_name: str, label: str, celsius: float) -> bool:
|
||||
"""Whether a sensor's reading may count as the host's temperature: not one
|
||||
judged unwired from how it moves against the CPU (sensors.py, #5783),
|
||||
and a reading counts_as_hottest allows."""
|
||||
from . import sensors
|
||||
return label not in sensors.unwired(host_name) and counts_as_hottest(label, celsius)
|
||||
|
||||
|
||||
def split_temps(host_name: str, temps: dict) -> tuple[dict, dict]:
|
||||
"""(real, unwired): a host's sensors, hottest first, apart from the ones
|
||||
counts_as_hottest leaves out (a board input with nothing wired to it, a
|
||||
reading at or below 0 °C). Those read a constant at any load, so the
|
||||
page lists them apart instead of colouring them as the host's heat."""
|
||||
def by_heat(d: dict) -> dict:
|
||||
return dict(sorted(d.items(), key=lambda kv: -(kv[1] if kv[1] is not None else -999)))
|
||||
real = {k: c for k, c in temps.items() if c is None or counts_as_hottest(k, c)}
|
||||
return by_heat(real), by_heat({k: c for k, c in temps.items() if k not in real})
|
||||
is_thermometer leaves out. Unwired ones map to (reading, why); they read
|
||||
a constant or run against the CPU, so the page lists them apart instead
|
||||
of colouring them as the host's heat."""
|
||||
from . import sensors
|
||||
judged = sensors.unwired(host_name)
|
||||
|
||||
def by_heat(d: dict, value=lambda v: v) -> dict:
|
||||
return dict(sorted(d.items(), key=lambda kv: -(value(kv[1]) if value(kv[1]) is not None else -999)))
|
||||
real = {k: c for k, c in temps.items() if c is None or is_thermometer(host_name, k, c)}
|
||||
unwired = {k: (c, judged.get(k, "reads a value no wired sensor gives"))
|
||||
for k, c in temps.items() if k not in real}
|
||||
return by_heat(real), by_heat(unwired, lambda v: v[0])
|
||||
|
||||
|
||||
def _ts_or(value, fallback: datetime) -> datetime:
|
||||
@@ -305,14 +318,15 @@ def _expand_sample_to_metrics(
|
||||
row("disk_write_bps", host_name, dw_total)
|
||||
|
||||
# Temperatures — per sensor plus the host max for at-a-glance. The max
|
||||
# counts only readings that can be real (#5759).
|
||||
# counts only readings that can be real (#5759), from sensors that behave
|
||||
# like thermometers (#5783).
|
||||
max_temp: float | None = None
|
||||
for t in sample.get("temps") or []:
|
||||
c = t.get("celsius")
|
||||
if c is None:
|
||||
continue
|
||||
row("temp_c", f"{host_name}:temp:{t['label']}", c)
|
||||
if counts_as_hottest(str(t["label"]), c) and (max_temp is None or c > max_temp):
|
||||
if is_thermometer(host_name, str(t["label"]), c) and (max_temp is None or c > max_temp):
|
||||
max_temp = c
|
||||
if max_temp is not None:
|
||||
row("temp_c_max", host_name, max_temp)
|
||||
@@ -867,7 +881,7 @@ async def host_detail_metrics(host_id: str):
|
||||
HostAgentRegistration.host_id == host_id))).scalar_one_or_none()
|
||||
latest = await _latest_metrics_for_host(session, host.name)
|
||||
hostlvl, cores, nets, disks_io, temps, mounts = _split_host_metrics(host.name, latest)
|
||||
temps, unwired = split_temps(temps)
|
||||
temps, unwired = split_temps(host.name, temps)
|
||||
ls = reg.last_seen_at if reg else None
|
||||
stale = ls is None or (datetime.now(timezone.utc) - ls).total_seconds() > _stale_after_seconds()
|
||||
return await render_template(
|
||||
|
||||
Reference in New Issue
Block a user