use crate::api::devices::{GetDeviceDetails, GetDeviceStatistics, GetDevices};
use crate::error::{Error, Result};
use crate::models::{DeviceDetails, DeviceStatistics, SiteDevice};
use crate::stats::{aggregate_clients_by_device, DeviceClientStats};
use crate::UnifiClient;
use futures::stream::{self, StreamExt};
use std::collections::HashMap;
const MAX_CONCURRENT_REQUESTS: usize = 10;
#[derive(Debug, Clone)]
pub struct DeviceWithInfo {
pub device: SiteDevice,
pub details: DeviceDetails,
pub statistics: DeviceStatistics,
}
impl DeviceWithInfo {
pub fn new(device: SiteDevice, details: DeviceDetails, statistics: DeviceStatistics) -> Self {
Self {
device,
details,
statistics,
}
}
pub fn id(&self) -> &str {
&self.device.id
}
pub fn name(&self) -> &str {
&self.device.name
}
pub fn model(&self) -> &str {
&self.device.model
}
pub fn is_online(&self) -> bool {
self.device.is_online()
}
pub fn is_access_point(&self) -> bool {
self.device.is_access_point()
}
pub fn is_gateway(&self) -> bool {
self.device.is_gateway()
}
pub fn has_switching(&self) -> bool {
self.device.has_switching()
}
pub fn port_count(&self) -> usize {
self.details.port_count()
}
pub fn radio_count(&self) -> usize {
self.details.radio_count()
}
pub fn uptime_sec(&self) -> u64 {
self.statistics.uptime_sec
}
pub fn uptime_formatted(&self) -> String {
self.statistics.uptime_formatted()
}
pub fn cpu_utilization(&self) -> Option<f64> {
self.statistics.cpu_utilization_pct
}
pub fn memory_utilization(&self) -> Option<f64> {
self.statistics.memory_utilization_pct
}
}
impl UnifiClient {
pub async fn fetch_device_with_info(
&self,
site_id: &str,
device_id: &str,
) -> Result<DeviceWithInfo> {
let devices_endpoint = GetDevices::new(site_id);
let details_endpoint = GetDeviceDetails::new(site_id, device_id);
let stats_endpoint = GetDeviceStatistics::new(site_id, device_id);
let (devices_result, details_result, stats_result) = tokio::join!(
self.execute(&devices_endpoint),
self.execute(&details_endpoint),
self.execute(&stats_endpoint),
);
let devices_response = devices_result?;
let device = devices_response
.data
.into_iter()
.find(|d| d.id == device_id)
.ok_or_else(|| Error::NotFound(format!("Device {} not found", device_id)))?;
let details = details_result?;
let statistics = stats_result?;
Ok(DeviceWithInfo::new(device, details, statistics))
}
pub async fn fetch_all_devices_with_info(&self, site_id: &str) -> Result<Vec<DeviceWithInfo>> {
let devices = self.fetch_all_devices(site_id).await?;
let results: Vec<Result<DeviceWithInfo>> = stream::iter(devices)
.map(|device| {
let site_id = site_id.to_string();
let device_id = device.id.clone();
async move {
let details_endpoint = GetDeviceDetails::new(&site_id, &device_id);
let stats_endpoint = GetDeviceStatistics::new(&site_id, &device_id);
let (details_result, stats_result) = tokio::join!(
self.execute(&details_endpoint),
self.execute(&stats_endpoint),
);
let details = details_result?;
let statistics = stats_result?;
Ok(DeviceWithInfo::new(device, details, statistics))
}
})
.buffer_unordered(MAX_CONCURRENT_REQUESTS)
.collect()
.await;
results.into_iter().collect()
}
pub async fn fetch_client_stats_by_device(
&self,
site_id: &str,
) -> Result<HashMap<String, DeviceClientStats>> {
let clients = self.fetch_all_clients(site_id).await?;
Ok(aggregate_clients_by_device(&clients))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::common::MacAddress;
use crate::models::device_details::{DeviceFeatures, PhysicalInterfaces};
use crate::models::{DeviceFeature, DeviceInterface, DeviceState};
fn make_site_device() -> SiteDevice {
SiteDevice {
id: "device-123".to_string(),
mac_address: MacAddress::default(),
ip_address: None,
name: "Test Device".to_string(),
model: "U6-Pro".to_string(),
state: DeviceState::Online,
supported: Some(true),
firmware_version: Some("6.0.0".to_string()),
firmware_updatable: Some(false),
features: vec![DeviceFeature::AccessPoint],
interfaces: vec![DeviceInterface::Radios],
}
}
fn make_device_details() -> DeviceDetails {
DeviceDetails {
id: "device-123".to_string(),
mac_address: MacAddress::default(),
ip_address: crate::models::common::IpAddress::default(),
name: "Test Device".to_string(),
model: "U6-Pro".to_string(),
supported: true,
state: "ONLINE".to_string(),
firmware_version: Some("6.0.0".to_string()),
firmware_updatable: false,
adopted_at: None,
provisioned_at: None,
configuration_id: "config-123".to_string(),
uplink: None,
features: DeviceFeatures::default(),
interfaces: PhysicalInterfaces::default(),
}
}
fn make_device_statistics() -> DeviceStatistics {
DeviceStatistics {
uptime_sec: 86400,
last_heartbeat_at: None,
next_heartbeat_at: None,
load_average_1_min: Some(0.5),
load_average_5_min: Some(0.4),
load_average_15_min: Some(0.3),
cpu_utilization_pct: Some(25.0),
memory_utilization_pct: Some(50.0),
uplink: None,
interfaces: None,
}
}
#[test]
fn test_device_with_info_accessors() {
let device = make_site_device();
let details = make_device_details();
let statistics = make_device_statistics();
let info = DeviceWithInfo::new(device, details, statistics);
assert_eq!(info.id(), "device-123");
assert_eq!(info.name(), "Test Device");
assert_eq!(info.model(), "U6-Pro");
assert!(info.is_online());
assert!(info.is_access_point());
assert!(!info.is_gateway());
assert!(!info.has_switching());
assert_eq!(info.uptime_sec(), 86400);
assert_eq!(info.cpu_utilization(), Some(25.0));
assert_eq!(info.memory_utilization(), Some(50.0));
}
#[test]
fn test_device_with_info_uptime_formatted() {
let device = make_site_device();
let details = make_device_details();
let mut statistics = make_device_statistics();
statistics.uptime_sec = 90061;
let info = DeviceWithInfo::new(device, details, statistics);
assert_eq!(info.uptime_formatted(), "1d 1h 1m 1s");
}
}