From 6f46915eeb1f2dc49688be8a807d53d7fee9758b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 11 May 2026 12:13:09 -0500 Subject: [PATCH] fix(insights): raise device overheating thresholds for outdoor equipment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/towerops/llm/insight_prompt.ex | 2 +- lib/towerops/llm/network_snapshot.ex | 10 ++++---- .../rules/device_overheating.ex | 7 ++++-- .../rules/device_overheating_test.exs | 24 +++++++++---------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/towerops/llm/insight_prompt.ex b/lib/towerops/llm/insight_prompt.ex index 9265ffdd..c1930269 100644 --- a/lib/towerops/llm/insight_prompt.ex +++ b/lib/towerops/llm/insight_prompt.ex @@ -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 diff --git a/lib/towerops/llm/network_snapshot.ex b/lib/towerops/llm/network_snapshot.ex index d6d0c1ca..329d3a0e 100644 --- a/lib/towerops/llm/network_snapshot.ex +++ b/lib/towerops/llm/network_snapshot.ex @@ -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}) diff --git a/lib/towerops/recommendations/rules/device_overheating.ex b/lib/towerops/recommendations/rules/device_overheating.ex index 76ca51d4..3e8671ec 100644 --- a/lib/towerops/recommendations/rules/device_overheating.ex +++ b/lib/towerops/recommendations/rules/device_overheating.ex @@ -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 diff --git a/test/towerops/recommendations/rules/device_overheating_test.exs b/test/towerops/recommendations/rules/device_overheating_test.exs index 7f2d1ce2..21b7cc7f 100644 --- a/test/towerops/recommendations/rules/device_overheating_test.exs +++ b/test/towerops/recommendations/rules/device_overheating_test.exs @@ -51,21 +51,21 @@ defmodule Towerops.Recommendations.Rules.DeviceOverheatingTest do assert DeviceOverheating.evaluate(org.id) == [] end - test "warning insight at 65–75°C", %{org: org, device: device, snmp: snmp} do - insert_temp(snmp, 68.0) + test "warning insight at 80–90°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