On the live instance the hourly refresh took 3 min 8 s, averaging a week of raw temp_c samples each time. The hourly averages are now kept in memory. The first refresh after startup reads the whole window. Each later one reads raw samples from the start of the hour the last read ended in, so the hour that was still filling is read again whole. merge_hours folds the new rows in and drops hours older than the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
4.1 KiB
Python
85 lines
4.1 KiB
Python
"""Which temperature sensors are thermometers, judged from how each moves
|
||
against the host's CPU (#5783). VDNT-Util01's SYSTIN ran at r = −0.81
|
||
against its CPU package over a week; CPUTIN beside it at +0.83 (#5782)."""
|
||
|
||
import math
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
from plugins.host_agent import sensors as s
|
||
|
||
T0 = datetime(2026, 10, 8, tzinfo=timezone.utc)
|
||
|
||
|
||
def _hours(n, f):
|
||
"""{hour: f(i)} for n hourly buckets."""
|
||
return {T0 + timedelta(hours=i): f(i) for i in range(n)}
|
||
|
||
|
||
def cpu(i): # a CPU that swings 50-80 °C through the day
|
||
return 65 + 15 * math.sin(i / 24 * 2 * math.pi)
|
||
|
||
|
||
def test_a_sensor_that_falls_as_the_cpu_warms_is_not_a_thermometer():
|
||
ref = _hours(96, cpu)
|
||
systin = _hours(96, lambda i: 114 - 0.1 * (cpu(i) - 65)) # Util01's shape
|
||
why = s.judge(ref, systin)
|
||
assert why.startswith("falls as the CPU warms (r -1.00 over 96 h")
|
||
|
||
|
||
def test_a_sensor_that_holds_still_is_not_a_thermometer():
|
||
why = s.judge(_hours(96, cpu), _hours(96, lambda i: 110.0 + (0.1 if i % 2 else 0.0)))
|
||
assert why == "holds at 110 °C while the CPU moved 50–80 °C"
|
||
|
||
|
||
def test_a_wired_sensor_is_left_alone_even_when_it_barely_follows_the_cpu():
|
||
ref = _hours(96, cpu)
|
||
assert s.judge(ref, _hours(96, lambda i: 40 + 0.3 * (cpu(i) - 65))) is None # a board sensor
|
||
assert s.judge(ref, _hours(96, lambda i: 45 + 3 * math.sin(i))) is None # a drive, its own rhythm
|
||
|
||
|
||
def test_nothing_is_judged_without_enough_history_or_a_cpu_that_moved():
|
||
falls = lambda i: 114 - 0.1 * (cpu(i) - 65) # noqa: E731
|
||
assert s.judge(_hours(48, cpu), _hours(48, falls)) is None # 48 h < MIN_HOURS
|
||
idle = _hours(96, lambda i: 50 + (i % 3)) # moved 2 °C
|
||
assert s.judge(idle, _hours(96, lambda i: 110 - (i % 3))) is None
|
||
|
||
|
||
def test_the_cpu_reference_is_found_by_its_driver_names():
|
||
assert s.reference_label(["SYSTIN", "Core 0", "Package id 1", "Package id 0"]) == "Package id 0"
|
||
assert s.reference_label(["Tccd1", "Tctl", "Composite"]) == "Tctl"
|
||
assert s.reference_label(["PECI Agent 0", "SYSTIN"]) == "PECI Agent 0"
|
||
assert s.reference_label(["Composite", "temp1"]) is None
|
||
|
||
|
||
def test_hosts_are_judged_from_hourly_rows_and_a_host_without_a_cpu_reading_is_skipped():
|
||
rows = []
|
||
for i in range(96):
|
||
h = T0 + timedelta(hours=i)
|
||
rows += [("util:temp:Package id 0", h, cpu(i)),
|
||
("util:temp:SYSTIN", h, 114 - 0.1 * (cpu(i) - 65)),
|
||
("util:temp:CPUTIN", h, 40 + 0.3 * (cpu(i) - 65)),
|
||
("nas:temp:temp1", h, 110 - 0.1 * (cpu(i) - 65)),
|
||
("util", h, 1.0)] # not a sensor row
|
||
got = s.judge_hosts(rows)
|
||
assert list(got) == ["util"] and list(got["util"]) == ["SYSTIN"]
|
||
|
||
|
||
def test_a_judged_sensor_is_left_out_of_the_host_temperature_and_listed_with_why(monkeypatch):
|
||
from plugins.host_agent.routes import is_thermometer, split_temps
|
||
monkeypatch.setattr(s, "_unwired", {"util": {"temp3": "falls as the CPU warms (r -0.81 over 168 h)"}})
|
||
assert not is_thermometer("util", "temp3", 62.0) # a plausible value, still not a thermometer
|
||
assert is_thermometer("other", "temp3", 62.0)
|
||
real, unwired = split_temps("util", {"temp3": 62.0, "Package id 0": 55.0})
|
||
assert list(real) == ["Package id 0"]
|
||
assert unwired == {"temp3": (62.0, "falls as the CPU warms (r -0.81 over 168 h)")}
|
||
|
||
|
||
def test_a_later_read_replaces_the_hour_that_was_filling_and_old_hours_drop():
|
||
hours = {"h:temp:a": {T0: 50.0, T0 + timedelta(hours=1): 51.0}}
|
||
s.merge_hours(hours, [("h:temp:a", T0 + timedelta(hours=1), 55.0), # the hour, read whole
|
||
("h:temp:a", T0 + timedelta(hours=2), 56.0),
|
||
("h:temp:b", T0 + timedelta(hours=2), None)], since=T0 + timedelta(hours=1))
|
||
assert hours == {"h:temp:a": {T0 + timedelta(hours=1): 55.0, T0 + timedelta(hours=2): 56.0}}
|
||
s.merge_hours(hours, [], since=T0 + timedelta(hours=9)) # a sensor gone quiet
|
||
assert hours == {}
|