diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d256735..130430d1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,18 @@ CHANGELOG - towerops-web ======================== +2026-02-11 - fix: normalize OIDs so agent-polled sensors match regardless of leading dot + - File: lib/towerops_web/channels/agent_channel.ex + Agent (gosnmp) returns OIDs with leading dots (e.g. ".1.3.6.1.4.1.41112...") + but sensor OIDs stored in DB may omit the dot. process_sensor_reading used + direct Map.get which silently missed wireless/vendor sensors on Ubiquiti airOS. + - Fix: Added normalize_oid_keys/1 to strip leading dots from all oid_values keys, + and normalize sensor.sensor_oid too. Both sides now match without leading dot. + - File: test/towerops/snmp/sensor_change_detector_test.exs + Fixed flaky refute_receive tests that subscribed to global "device:events" topic + and caught unrelated device_discovered events from concurrent async tests. + Changed to device-specific topic "device:#{device.id}" instead. + 2026-02-11 - feat: add sensor value change events for all sensor types - New file: lib/towerops/snmp/sensor_change_detector.ex Extracted shared sensor change detection logic from DevicePollerWorker. diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index 80670fcd..28473678 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -1377,7 +1377,7 @@ defmodule ToweropsWeb.AgentChannel do Devices.update_snmp_poll_time(device) snmp_device = device.snmp_device - oid_values = Map.new(result.oid_values) + oid_values = normalize_oid_keys(result.oid_values) # Use server time to avoid clock drift issues between agents timestamp = DateTime.utc_now() @@ -1534,7 +1534,9 @@ defmodule ToweropsWeb.AgentChannel do end defp process_sensor_reading(sensor, oid_values, timestamp) do - case Map.get(oid_values, sensor.sensor_oid) do + normalized_oid = String.trim_leading(sensor.sensor_oid, ".") + + case Map.get(oid_values, normalized_oid) do nil -> :ok @@ -1586,6 +1588,16 @@ defmodule ToweropsWeb.AgentChannel do defp parse_integer(_), do: nil + # Normalize OID keys to strip leading dots. + # The agent (via gosnmp/net-snmp) returns OIDs with leading dots + # (e.g., ".1.3.6.1.2.1.1.1.0") but sensor OIDs and hardcoded interface + # OIDs use the format without (e.g., "1.3.6.1.2.1.1.1.0"). + defp normalize_oid_keys(oid_values) do + Map.new(oid_values, fn {oid, value} -> + {String.trim_leading(oid, "."), value} + end) + end + defp parse_float(value) when is_float(value), do: value defp parse_float(value) when is_integer(value), do: value / 1.0 diff --git a/test/towerops/snmp/sensor_change_detector_test.exs b/test/towerops/snmp/sensor_change_detector_test.exs index dbb183a2..aef9f3e6 100644 --- a/test/towerops/snmp/sensor_change_detector_test.exs +++ b/test/towerops/snmp/sensor_change_detector_test.exs @@ -106,23 +106,22 @@ defmodule Towerops.Snmp.SensorChangeDetectorTest do assert String.contains?(event.message, "46.0C") end - test "does not emit event when value is unchanged", %{snmp_device: snmp_device} do + test "does not emit event when value is unchanged", %{device: device, snmp_device: snmp_device} do sensor = create_sensor(snmp_device, %{last_value: 50.0, sensor_unit: "Hz"}) timestamp = DateTime.utc_now() - # Subscribe to both topics - Phoenix.PubSub.subscribe(Towerops.PubSub, "device:events") + Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}") SensorChangeDetector.detect_and_broadcast(sensor, 50.0, timestamp) refute_receive {:device_event, _}, 100 end - test "does not emit sensor_value_changed when last_value is nil", %{snmp_device: snmp_device} do + test "does not emit sensor_value_changed when last_value is nil", %{device: device, snmp_device: snmp_device} do sensor = create_sensor(snmp_device, %{last_value: nil, sensor_unit: "Hz"}) timestamp = DateTime.utc_now() - Phoenix.PubSub.subscribe(Towerops.PubSub, "device:events") + Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}") SensorChangeDetector.detect_and_broadcast(sensor, 50.0, timestamp) @@ -177,7 +176,7 @@ defmodule Towerops.Snmp.SensorChangeDetectorTest do assert event.severity == "info" end - test "does not emit spike/drop for % sensor with <30% change", %{snmp_device: snmp_device} do + test "does not emit spike/drop for % sensor with <30% change", %{device: device, snmp_device: snmp_device} do sensor = create_sensor(snmp_device, %{ sensor_type: "percent", @@ -189,7 +188,7 @@ defmodule Towerops.Snmp.SensorChangeDetectorTest do timestamp = DateTime.utc_now() - Phoenix.PubSub.subscribe(Towerops.PubSub, "device:events") + Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}") SensorChangeDetector.detect_and_broadcast(sensor, 55.0, timestamp)