Count distinct devices (by MAC) in active scanner status

The ARP and SNMP scanners reported every online sighting, so a device
seen on multiple IPs or via duplicate ARP replies was counted more than
once. Fold the dedup into ActiveStatus::record_scan, which now takes the
device slice and reports the number of distinct MAC addresses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-05 21:19:06 -04:00
co-authored by Claude Opus 4.8
parent 844d7d189c
commit 6b044a6739
6 changed files with 45 additions and 12 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
info!("Done with ARP probes");
info!("Found {} online devices", devices.len());
status::record_scan(devices.len() as u64);
status::record_scan(&devices);
// Process found devices
for device in devices.iter() {
+3 -2
View File
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::model::devices::Device;
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
@@ -22,9 +23,9 @@ pub fn set_waiting(next_scan_at: DateTime<Utc>) {
}
}
pub fn record_scan(devices_seen: u64) {
pub fn record_scan(devices: &[Device]) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_scan(devices_seen);
m.lock().unwrap().record_scan(devices);
}
}
+36 -5
View File
@@ -1,4 +1,6 @@
use crate::model::devices::Device;
use chrono::{DateTime, Utc};
use std::collections::HashSet;
/// Status state for the active (polling) scanners — ARP and SNMP. Each scanner owns its own
/// `OnceCell<Mutex<ActiveStatus>>` and delegates to these methods (see e.g.
@@ -41,9 +43,16 @@ impl ActiveStatus {
self.next_scan_at = Some(next_scan_at);
}
/// Record the result of a completed scan.
pub fn record_scan(&mut self, devices_seen: u64) {
self.last_scan_devices_seen = Some(devices_seen);
/// Record the result of a completed scan. A single scan can surface the same device more than
/// once — duplicate ARP replies, or one MAC bound to several IPs in an SNMP ARP cache — so the
/// reported count is the number of distinct devices (by MAC address), not raw sightings.
pub fn record_scan(&mut self, devices: &[Device]) {
let distinct = devices
.iter()
.map(|device| &device.mac_address)
.collect::<HashSet<_>>()
.len();
self.last_scan_devices_seen = Some(distinct as u64);
self.last_scan_at = Some(Utc::now());
}
@@ -83,15 +92,37 @@ mod tests {
assert_eq!(snapshot.next_scan_at.unwrap(), next);
}
fn device_with_mac(mac: &str, ip: &str) -> Device {
Device::new(mac.to_string(), ip.to_string(), String::new(), Utc::now())
}
#[test]
fn record_scan_stores_count_and_time() {
let mut status = ActiveStatus::new();
status.record_scan(7);
let devices = vec![
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.2"),
device_with_mac("aa:bb:cc:dd:ee:02", "192.168.0.3"),
];
status.record_scan(&devices);
let snapshot = status.snapshot();
assert_eq!(snapshot.last_scan_devices_seen, Some(7));
assert_eq!(snapshot.last_scan_devices_seen, Some(2));
assert!(snapshot.last_scan_at.is_some());
}
#[test]
fn record_scan_counts_distinct_devices() {
let mut status = ActiveStatus::new();
// Same MAC seen twice (e.g. duplicate ARP reply or one MAC on two IPs) plus a second
// distinct device — only two unique devices should be reported.
let devices = vec![
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.2"),
device_with_mac("aa:bb:cc:dd:ee:01", "192.168.0.9"),
device_with_mac("aa:bb:cc:dd:ee:02", "192.168.0.3"),
];
status.record_scan(&devices);
assert_eq!(status.snapshot().last_scan_devices_seen, Some(2));
}
#[test]
fn initial_state_is_empty() {
let snapshot = ActiveStatus::new().snapshot();
+1 -1
View File
@@ -32,7 +32,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
match finder::find(config).await {
Ok(devices) => {
info!("SNMP poll found {} devices in the ARP cache", devices.len());
status::record_scan(devices.len() as u64);
status::record_scan(&devices);
for device in devices.iter() {
pipeline::record_sighting(device.clone(), DeviceEventScanner::Snmp);
}
+3 -2
View File
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::model::devices::Device;
use crate::scanners::common::active_status::{ActiveSnapshot, ActiveStatus};
static STATUS: OnceCell<Mutex<ActiveStatus>> = OnceCell::new();
@@ -22,9 +23,9 @@ pub fn set_waiting(next_scan_at: DateTime<Utc>) {
}
}
pub fn record_scan(devices_seen: u64) {
pub fn record_scan(devices: &[Device]) {
if let Some(m) = STATUS.get() {
m.lock().unwrap().record_scan(devices_seen);
m.lock().unwrap().record_scan(devices);
}
}