mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Count passive scanner devices seen in the last hour
Replace the lifetime "devices seen since start" counter in the mDNS, SSDP
and DHCP scanners with a rolling count of distinct devices (deduped by
MAC) seen within the last hour.
Each scanner's status now tracks a MAC -> last-seen-time map; the snapshot
prunes entries older than 60 minutes and reports the remaining count. The
API field name (devices_seen) is unchanged, so only its meaning and the
frontend labels ("N devices in the last hour") are updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d15411a889
commit
72dbbe5e9a
@@ -10,7 +10,8 @@
|
|||||||
- [x] Add configuration options to enable/disable each scanner
|
- [x] Add configuration options to enable/disable each scanner
|
||||||
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
||||||
- [x] Add "devices seen on last scan" to the ARP and SNMP scanners status
|
- [x] Add "devices seen on last scan" to the ARP and SNMP scanners status
|
||||||
- [ ] Modify the passive scanners status to count devices seen in the last hour
|
- [x] Modify the passive scanners status to count devices seen in the last hour
|
||||||
|
- [ ] Simplify the code of scanners (repeated logic)
|
||||||
- [ ] Modify the event management to ignore duplicate events if they happen within a time threshold
|
- [ ] Modify the event management to ignore duplicate events if they happen within a time threshold
|
||||||
- [x] Review the recommended timings for the ARP scanner (change defaults) — SNMP defaults set to 10m/5s
|
- [x] Review the recommended timings for the ARP scanner (change defaults) — SNMP defaults set to 10m/5s
|
||||||
|
|
||||||
|
|||||||
@@ -99,5 +99,5 @@ async fn process_discovery(discovery: finder::DhcpDiscovery) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status::record_discovery();
|
status::record_discovery(&mac);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// A device counts as "seen" only if its most recent sighting falls within this
|
||||||
|
/// rolling window.
|
||||||
|
const RECENT_WINDOW_SECONDS: i64 = 3600;
|
||||||
|
|
||||||
pub struct DhcpScannerStatus {
|
pub struct DhcpScannerStatus {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Most recent sighting time per device MAC, used to count the distinct
|
||||||
|
/// devices seen within the last hour.
|
||||||
|
pub recent_sightings: HashMap<String, DateTime<Utc>>,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +20,8 @@ pub struct DhcpScannerStatus {
|
|||||||
pub struct DhcpScannerStatusSnapshot {
|
pub struct DhcpScannerStatusSnapshot {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Distinct devices seen within the last hour.
|
||||||
|
pub devices_last_hour: u64,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +32,7 @@ pub fn init() {
|
|||||||
.set(Mutex::new(DhcpScannerStatus {
|
.set(Mutex::new(DhcpScannerStatus {
|
||||||
is_listening: false,
|
is_listening: false,
|
||||||
listening_since: None,
|
listening_since: None,
|
||||||
devices_discovered: 0,
|
recent_sightings: HashMap::new(),
|
||||||
last_discovery_at: None,
|
last_discovery_at: None,
|
||||||
}))
|
}))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -38,21 +46,24 @@ pub fn set_listening() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_discovery() {
|
pub fn record_discovery(mac: &str) {
|
||||||
if let Some(m) = STATUS.get() {
|
if let Some(m) = STATUS.get() {
|
||||||
|
let now = Utc::now();
|
||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.devices_discovered += 1;
|
s.recent_sightings.insert(mac.to_string(), now);
|
||||||
s.last_discovery_at = Some(Utc::now());
|
s.last_discovery_at = Some(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get() -> Option<DhcpScannerStatusSnapshot> {
|
pub fn get() -> Option<DhcpScannerStatusSnapshot> {
|
||||||
STATUS.get().map(|m| {
|
STATUS.get().map(|m| {
|
||||||
let s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
|
let cutoff = Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS);
|
||||||
|
s.recent_sightings.retain(|_, seen| *seen >= cutoff);
|
||||||
DhcpScannerStatusSnapshot {
|
DhcpScannerStatusSnapshot {
|
||||||
is_listening: s.is_listening,
|
is_listening: s.is_listening,
|
||||||
listening_since: s.listening_since,
|
listening_since: s.listening_since,
|
||||||
devices_discovered: s.devices_discovered,
|
devices_last_hour: s.recent_sightings.len() as u64,
|
||||||
last_discovery_at: s.last_discovery_at,
|
last_discovery_at: s.last_discovery_at,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,7 +78,7 @@ mod tests {
|
|||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.is_listening = false;
|
s.is_listening = false;
|
||||||
s.listening_since = None;
|
s.listening_since = None;
|
||||||
s.devices_discovered = 0;
|
s.recent_sightings.clear();
|
||||||
s.last_discovery_at = None;
|
s.last_discovery_at = None;
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
@@ -84,22 +95,47 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_record_discovery() {
|
fn test_same_mac_counts_once() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert_eq!(snapshot.devices_discovered, 2);
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
assert!(snapshot.last_discovery_at.is_some());
|
assert!(snapshot.last_discovery_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distinct_macs_counted() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
record_discovery("11:22:33:44:55:66");
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_old_sighting_excluded() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
// Backdate an entry beyond the window; it must not be counted.
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.recent_sightings.insert(
|
||||||
|
"11:22:33:44:55:66".to_string(),
|
||||||
|
Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS + 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_initial_state() {
|
fn test_initial_state() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert!(!snapshot.is_listening);
|
assert!(!snapshot.is_listening);
|
||||||
assert!(snapshot.listening_since.is_none());
|
assert!(snapshot.listening_since.is_none());
|
||||||
assert_eq!(snapshot.devices_discovered, 0);
|
assert_eq!(snapshot.devices_last_hour, 0);
|
||||||
assert!(snapshot.last_discovery_at.is_none());
|
assert!(snapshot.last_discovery_at.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,5 +123,5 @@ async fn process_announcement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status::record_discovery();
|
status::record_discovery(&mac);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// A device counts as "seen" only if its most recent sighting falls within this
|
||||||
|
/// rolling window.
|
||||||
|
const RECENT_WINDOW_SECONDS: i64 = 3600;
|
||||||
|
|
||||||
pub struct MdnsScannerStatus {
|
pub struct MdnsScannerStatus {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Most recent sighting time per device MAC, used to count the distinct
|
||||||
|
/// devices seen within the last hour.
|
||||||
|
pub recent_sightings: HashMap<String, DateTime<Utc>>,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +20,8 @@ pub struct MdnsScannerStatus {
|
|||||||
pub struct MdnsScannerStatusSnapshot {
|
pub struct MdnsScannerStatusSnapshot {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Distinct devices seen within the last hour.
|
||||||
|
pub devices_last_hour: u64,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +32,7 @@ pub fn init() {
|
|||||||
.set(Mutex::new(MdnsScannerStatus {
|
.set(Mutex::new(MdnsScannerStatus {
|
||||||
is_listening: false,
|
is_listening: false,
|
||||||
listening_since: None,
|
listening_since: None,
|
||||||
devices_discovered: 0,
|
recent_sightings: HashMap::new(),
|
||||||
last_discovery_at: None,
|
last_discovery_at: None,
|
||||||
}))
|
}))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -38,21 +46,24 @@ pub fn set_listening() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_discovery() {
|
pub fn record_discovery(mac: &str) {
|
||||||
if let Some(m) = STATUS.get() {
|
if let Some(m) = STATUS.get() {
|
||||||
|
let now = Utc::now();
|
||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.devices_discovered += 1;
|
s.recent_sightings.insert(mac.to_string(), now);
|
||||||
s.last_discovery_at = Some(Utc::now());
|
s.last_discovery_at = Some(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get() -> Option<MdnsScannerStatusSnapshot> {
|
pub fn get() -> Option<MdnsScannerStatusSnapshot> {
|
||||||
STATUS.get().map(|m| {
|
STATUS.get().map(|m| {
|
||||||
let s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
|
let cutoff = Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS);
|
||||||
|
s.recent_sightings.retain(|_, seen| *seen >= cutoff);
|
||||||
MdnsScannerStatusSnapshot {
|
MdnsScannerStatusSnapshot {
|
||||||
is_listening: s.is_listening,
|
is_listening: s.is_listening,
|
||||||
listening_since: s.listening_since,
|
listening_since: s.listening_since,
|
||||||
devices_discovered: s.devices_discovered,
|
devices_last_hour: s.recent_sightings.len() as u64,
|
||||||
last_discovery_at: s.last_discovery_at,
|
last_discovery_at: s.last_discovery_at,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,7 +78,7 @@ mod tests {
|
|||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.is_listening = false;
|
s.is_listening = false;
|
||||||
s.listening_since = None;
|
s.listening_since = None;
|
||||||
s.devices_discovered = 0;
|
s.recent_sightings.clear();
|
||||||
s.last_discovery_at = None;
|
s.last_discovery_at = None;
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
@@ -84,22 +95,47 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_record_discovery() {
|
fn test_same_mac_counts_once() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert_eq!(snapshot.devices_discovered, 2);
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
assert!(snapshot.last_discovery_at.is_some());
|
assert!(snapshot.last_discovery_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distinct_macs_counted() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
record_discovery("11:22:33:44:55:66");
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_old_sighting_excluded() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
// Backdate an entry beyond the window; it must not be counted.
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.recent_sightings.insert(
|
||||||
|
"11:22:33:44:55:66".to_string(),
|
||||||
|
Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS + 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_initial_state() {
|
fn test_initial_state() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert!(!snapshot.is_listening);
|
assert!(!snapshot.is_listening);
|
||||||
assert!(snapshot.listening_since.is_none());
|
assert!(snapshot.listening_since.is_none());
|
||||||
assert_eq!(snapshot.devices_discovered, 0);
|
assert_eq!(snapshot.devices_last_hour, 0);
|
||||||
assert!(snapshot.last_discovery_at.is_none());
|
assert!(snapshot.last_discovery_at.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,5 +123,5 @@ async fn process_announcement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status::record_discovery();
|
status::record_discovery(&mac);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// A device counts as "seen" only if its most recent sighting falls within this
|
||||||
|
/// rolling window.
|
||||||
|
const RECENT_WINDOW_SECONDS: i64 = 3600;
|
||||||
|
|
||||||
pub struct SsdpScannerStatus {
|
pub struct SsdpScannerStatus {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Most recent sighting time per device MAC, used to count the distinct
|
||||||
|
/// devices seen within the last hour.
|
||||||
|
pub recent_sightings: HashMap<String, DateTime<Utc>>,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +20,8 @@ pub struct SsdpScannerStatus {
|
|||||||
pub struct SsdpScannerStatusSnapshot {
|
pub struct SsdpScannerStatusSnapshot {
|
||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
pub listening_since: Option<DateTime<Utc>>,
|
pub listening_since: Option<DateTime<Utc>>,
|
||||||
pub devices_discovered: u64,
|
/// Distinct devices seen within the last hour.
|
||||||
|
pub devices_last_hour: u64,
|
||||||
pub last_discovery_at: Option<DateTime<Utc>>,
|
pub last_discovery_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +32,7 @@ pub fn init() {
|
|||||||
.set(Mutex::new(SsdpScannerStatus {
|
.set(Mutex::new(SsdpScannerStatus {
|
||||||
is_listening: false,
|
is_listening: false,
|
||||||
listening_since: None,
|
listening_since: None,
|
||||||
devices_discovered: 0,
|
recent_sightings: HashMap::new(),
|
||||||
last_discovery_at: None,
|
last_discovery_at: None,
|
||||||
}))
|
}))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -38,21 +46,24 @@ pub fn set_listening() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_discovery() {
|
pub fn record_discovery(mac: &str) {
|
||||||
if let Some(m) = STATUS.get() {
|
if let Some(m) = STATUS.get() {
|
||||||
|
let now = Utc::now();
|
||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.devices_discovered += 1;
|
s.recent_sightings.insert(mac.to_string(), now);
|
||||||
s.last_discovery_at = Some(Utc::now());
|
s.last_discovery_at = Some(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get() -> Option<SsdpScannerStatusSnapshot> {
|
pub fn get() -> Option<SsdpScannerStatusSnapshot> {
|
||||||
STATUS.get().map(|m| {
|
STATUS.get().map(|m| {
|
||||||
let s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
|
let cutoff = Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS);
|
||||||
|
s.recent_sightings.retain(|_, seen| *seen >= cutoff);
|
||||||
SsdpScannerStatusSnapshot {
|
SsdpScannerStatusSnapshot {
|
||||||
is_listening: s.is_listening,
|
is_listening: s.is_listening,
|
||||||
listening_since: s.listening_since,
|
listening_since: s.listening_since,
|
||||||
devices_discovered: s.devices_discovered,
|
devices_last_hour: s.recent_sightings.len() as u64,
|
||||||
last_discovery_at: s.last_discovery_at,
|
last_discovery_at: s.last_discovery_at,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,7 +78,7 @@ mod tests {
|
|||||||
let mut s = m.lock().unwrap();
|
let mut s = m.lock().unwrap();
|
||||||
s.is_listening = false;
|
s.is_listening = false;
|
||||||
s.listening_since = None;
|
s.listening_since = None;
|
||||||
s.devices_discovered = 0;
|
s.recent_sightings.clear();
|
||||||
s.last_discovery_at = None;
|
s.last_discovery_at = None;
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
@@ -84,22 +95,47 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_record_discovery() {
|
fn test_same_mac_counts_once() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
record_discovery();
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert_eq!(snapshot.devices_discovered, 2);
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
assert!(snapshot.last_discovery_at.is_some());
|
assert!(snapshot.last_discovery_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distinct_macs_counted() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
record_discovery("11:22:33:44:55:66");
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_old_sighting_excluded() {
|
||||||
|
reset_for_test();
|
||||||
|
record_discovery("aa:bb:cc:dd:ee:ff");
|
||||||
|
// Backdate an entry beyond the window; it must not be counted.
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.recent_sightings.insert(
|
||||||
|
"11:22:33:44:55:66".to_string(),
|
||||||
|
Utc::now() - Duration::seconds(RECENT_WINDOW_SECONDS + 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert_eq!(snapshot.devices_last_hour, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_initial_state() {
|
fn test_initial_state() {
|
||||||
reset_for_test();
|
reset_for_test();
|
||||||
let snapshot = get().unwrap();
|
let snapshot = get().unwrap();
|
||||||
assert!(!snapshot.is_listening);
|
assert!(!snapshot.is_listening);
|
||||||
assert!(snapshot.listening_since.is_none());
|
assert!(snapshot.listening_since.is_none());
|
||||||
assert_eq!(snapshot.devices_discovered, 0);
|
assert_eq!(snapshot.devices_last_hour, 0);
|
||||||
assert!(snapshot.last_discovery_at.is_none());
|
assert!(snapshot.last_discovery_at.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct DhcpScannerStatusResponse {
|
|||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
/// Seconds the listener has been running (only set when is_listening is true)
|
/// Seconds the listener has been running (only set when is_listening is true)
|
||||||
pub listening_for_seconds: Option<f64>,
|
pub listening_for_seconds: Option<f64>,
|
||||||
/// Total device requests processed since the listener started
|
/// Distinct devices seen in the last hour
|
||||||
pub devices_seen: u64,
|
pub devices_seen: u64,
|
||||||
/// Seconds since the last device was seen (None if none seen yet)
|
/// Seconds since the last device was seen (None if none seen yet)
|
||||||
pub last_device_seen_seconds_ago: Option<f64>,
|
pub last_device_seen_seconds_ago: Option<f64>,
|
||||||
@@ -51,7 +51,7 @@ pub async fn status() -> Result<Json<DhcpScannerStatusResponse>, StatusCode> {
|
|||||||
Ok(Json(DhcpScannerStatusResponse {
|
Ok(Json(DhcpScannerStatusResponse {
|
||||||
is_listening: snapshot.is_listening,
|
is_listening: snapshot.is_listening,
|
||||||
listening_for_seconds,
|
listening_for_seconds,
|
||||||
devices_seen: snapshot.devices_discovered,
|
devices_seen: snapshot.devices_last_hour,
|
||||||
last_device_seen_seconds_ago,
|
last_device_seen_seconds_ago,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct MdnsScannerStatusResponse {
|
|||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
/// Seconds the listener has been running (only set when is_listening is true)
|
/// Seconds the listener has been running (only set when is_listening is true)
|
||||||
pub listening_for_seconds: Option<f64>,
|
pub listening_for_seconds: Option<f64>,
|
||||||
/// Total device announcements processed since the listener started
|
/// Distinct devices seen in the last hour
|
||||||
pub devices_seen: u64,
|
pub devices_seen: u64,
|
||||||
/// Seconds since the last device was seen (None if none seen yet)
|
/// Seconds since the last device was seen (None if none seen yet)
|
||||||
pub last_device_seen_seconds_ago: Option<f64>,
|
pub last_device_seen_seconds_ago: Option<f64>,
|
||||||
@@ -51,7 +51,7 @@ pub async fn status() -> Result<Json<MdnsScannerStatusResponse>, StatusCode> {
|
|||||||
Ok(Json(MdnsScannerStatusResponse {
|
Ok(Json(MdnsScannerStatusResponse {
|
||||||
is_listening: snapshot.is_listening,
|
is_listening: snapshot.is_listening,
|
||||||
listening_for_seconds,
|
listening_for_seconds,
|
||||||
devices_seen: snapshot.devices_discovered,
|
devices_seen: snapshot.devices_last_hour,
|
||||||
last_device_seen_seconds_ago,
|
last_device_seen_seconds_ago,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct SsdpScannerStatusResponse {
|
|||||||
pub is_listening: bool,
|
pub is_listening: bool,
|
||||||
/// Seconds the listener has been running (only set when is_listening is true)
|
/// Seconds the listener has been running (only set when is_listening is true)
|
||||||
pub listening_for_seconds: Option<f64>,
|
pub listening_for_seconds: Option<f64>,
|
||||||
/// Total device announcements processed since the listener started
|
/// Distinct devices seen in the last hour
|
||||||
pub devices_seen: u64,
|
pub devices_seen: u64,
|
||||||
/// Seconds since the last device was seen (None if none seen yet)
|
/// Seconds since the last device was seen (None if none seen yet)
|
||||||
pub last_device_seen_seconds_ago: Option<f64>,
|
pub last_device_seen_seconds_ago: Option<f64>,
|
||||||
@@ -51,7 +51,7 @@ pub async fn status() -> Result<Json<SsdpScannerStatusResponse>, StatusCode> {
|
|||||||
Ok(Json(SsdpScannerStatusResponse {
|
Ok(Json(SsdpScannerStatusResponse {
|
||||||
is_listening: snapshot.is_listening,
|
is_listening: snapshot.is_listening,
|
||||||
listening_for_seconds,
|
listening_for_seconds,
|
||||||
devices_seen: snapshot.devices_discovered,
|
devices_seen: snapshot.devices_last_hour,
|
||||||
last_device_seen_seconds_ago,
|
last_device_seen_seconds_ago,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ class DhcpScannerCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
final sublabels = <String>[
|
final sublabels = <String>[
|
||||||
status.listeningForSeconds != null
|
status.listeningForSeconds != null
|
||||||
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices seen'
|
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices in the last hour'
|
||||||
: '${status.devicesSeen} devices seen',
|
: '${status.devicesSeen} devices in the last hour',
|
||||||
if (status.lastDeviceSeenSecondsAgo != null)
|
if (status.lastDeviceSeenSecondsAgo != null)
|
||||||
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ class MdnsScannerCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
final sublabels = <String>[
|
final sublabels = <String>[
|
||||||
status.listeningForSeconds != null
|
status.listeningForSeconds != null
|
||||||
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices seen'
|
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices in the last hour'
|
||||||
: '${status.devicesSeen} devices seen',
|
: '${status.devicesSeen} devices in the last hour',
|
||||||
if (status.lastDeviceSeenSecondsAgo != null)
|
if (status.lastDeviceSeenSecondsAgo != null)
|
||||||
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ class SsdpScannerCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
final sublabels = <String>[
|
final sublabels = <String>[
|
||||||
status.listeningForSeconds != null
|
status.listeningForSeconds != null
|
||||||
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices seen'
|
? 'Listening for ${formatSeconds(status.listeningForSeconds! + elapsed)} · ${status.devicesSeen} devices in the last hour'
|
||||||
: '${status.devicesSeen} devices seen',
|
: '${status.devicesSeen} devices in the last hour',
|
||||||
if (status.lastDeviceSeenSecondsAgo != null)
|
if (status.lastDeviceSeenSecondsAgo != null)
|
||||||
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
'Last device ${formatSeconds(status.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user