fix(insights): raise device overheating thresholds for outdoor equipment

Warning: 65°C → 80°C, Critical: 75°C → 90°C. Outdoor gear in Texas
routinely runs at 70-75°C ambient. The old thresholds generated useless
AI insights like "Verona to Altoga at 72°C may be overheating" — that's
normal for an enclosure in summer, not actionable.
This commit is contained in:
Graham McIntire 2026-05-11 12:13:09 -05:00
parent e6e1326689
commit 6f46915eeb
4 changed files with 24 additions and 19 deletions

View file

@ -121,7 +121,7 @@ defmodule Towerops.LLM.InsightPrompt do
""",
"device_overheating" => """
Rule context: a device has at least one temperature sensor above
65 °C (warning) or 75 °C (critical). Metadata fields:
80 °C (warning) or 90 °C (critical). Metadata fields:
temperature_c, sensor_descr, qoe_score, warning_threshold_c,
critical_threshold_c.
If qoe_score is also low (<60), highlight that the QoE drop is

View file

@ -187,16 +187,18 @@ defmodule Towerops.LLM.NetworkSnapshot do
|> Repo.all()
end
# Devices with at least one temperature sensor reading > 60°C in the
# last hour. The `last_value` field on `Sensor` is updated on every
# poll, so we don't need a time-series join.
# Devices with at least one temperature sensor reading > 80°C.
# Outdoor equipment in hot climates routinely hits 70-75°C — we only
# surface devices that are approaching thermal-throttling territory.
# The `last_value` field on `Sensor` is updated on every poll, so we
# don't need a time-series join.
defp hottest_devices(organization_id) do
Sensor
|> join(:inner, [s], sd in SnmpDevice, on: s.snmp_device_id == sd.id)
|> join(:inner, [_s, sd], d in Device, on: sd.device_id == d.id)
|> where([s, _sd, d], d.organization_id == ^organization_id)
|> where([s], s.sensor_type == "temperature")
|> where([s], not is_nil(s.last_value) and s.last_value > 60.0)
|> where([s], not is_nil(s.last_value) and s.last_value > 80.0)
|> order_by([s], desc: s.last_value)
|> limit(@top_n)
|> select([s, _sd, d], %{device_id: d.id, name: d.name, max_temp_c: s.last_value})

View file

@ -23,8 +23,11 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheating do
alias Towerops.Snmp.Device, as: SnmpDevice
alias Towerops.Snmp.Sensor
@warning_threshold_c 65.0
@critical_threshold_c 75.0
# Outdoor equipment in hot climates routinely runs at 70-75°C ambient.
# Thresholds are set at levels where thermal throttling or hardware
# damage is actually imminent, not just "warm to the touch."
@warning_threshold_c 80.0
@critical_threshold_c 90.0
@qoe_concern_threshold 60.0
# Anything above this is implausible for routing/wireless gear and
# almost always indicates a missing divisor in the SNMP profile. Skip

View file

@ -51,21 +51,21 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheatingTest do
assert DeviceOverheating.evaluate(org.id) == []
end
test "warning insight at 6575°C", %{org: org, device: device, snmp: snmp} do
insert_temp(snmp, 68.0)
test "warning insight at 8090°C", %{org: org, device: device, snmp: snmp} do
insert_temp(snmp, 85.0)
assert [insight] = DeviceOverheating.evaluate(org.id)
assert insight.type == "device_overheating"
assert insight.urgency == "warning"
assert insight.device_id == device.id
assert insight.metadata["temperature_c"] == 68.0
assert insight.metadata["temperature_c"] == 85.0
assert insight.metadata["sensor_descr"] == "Enclosure"
assert insight.title =~ "AP-Hot"
assert insight.title =~ "68"
assert insight.title =~ "85"
end
test "critical insight at 75°C+", %{org: org, snmp: snmp} do
insert_temp(snmp, 78.0)
test "critical insight at 90°C+", %{org: org, snmp: snmp} do
insert_temp(snmp, 92.0)
assert [insight] = DeviceOverheating.evaluate(org.id)
assert insight.urgency == "critical"
@ -81,17 +81,17 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheatingTest do
end
test "uses the hottest sensor when multiple are present", %{org: org, snmp: snmp} do
insert_temp(snmp, 60.0, descr: "CPU")
insert_temp(snmp, 70.0, descr: "Enclosure")
insert_temp(snmp, 75.0, descr: "CPU")
insert_temp(snmp, 88.0, descr: "Enclosure")
assert [insight] = DeviceOverheating.evaluate(org.id)
assert insight.metadata["temperature_c"] == 70.0
assert insight.metadata["temperature_c"] == 88.0
assert insight.metadata["sensor_descr"] == "Enclosure"
end
test "correlates with Preseem QoE degradation when AP is linked",
%{org: org, device: device, snmp: snmp} do
insert_temp(snmp, 72.0)
insert_temp(snmp, 85.0)
{:ok, _ap} =
%AccessPoint{}
@ -112,7 +112,7 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheatingTest do
end
test "scopes to organization", %{snmp: snmp} do
insert_temp(snmp, 78.0)
insert_temp(snmp, 85.0)
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
@ -120,7 +120,7 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheatingTest do
end
test "ignores non-temperature sensors", %{org: org, snmp: snmp} do
insert_temp(snmp, 78.0, type: "voltage")
insert_temp(snmp, 85.0, type: "voltage")
assert DeviceOverheating.evaluate(org.id) == []
end
end