unifly-api 0.9.2

Async Rust client, reactive data layer, and domain model for UniFi controller APIs
Documentation
use std::collections::HashMap;

use crate::model::{Client, Device, MacAddress};
use crate::session::models::{SessionClientEntry, SessionDevice, SessionUserEntry};

use super::super::support::parse_session_device_wan_ipv6;

pub(super) fn merge_session_clients(
    clients: &mut [Client],
    session_clients: &[SessionClientEntry],
    session_users: &[SessionUserEntry],
) {
    merge_client_traffic(clients, session_clients);
    merge_user_reservations(clients, session_clients, session_users);
}

pub(super) fn merge_session_devices(devices: &mut [Device], session_devices: &[SessionDevice]) {
    if session_devices.is_empty() {
        return;
    }

    let session_by_mac: HashMap<&str, &SessionDevice> = session_devices
        .iter()
        .map(|device| (device.mac.as_str(), device))
        .collect();
    for device in devices {
        if let Some(legacy_device) = session_by_mac.get(device.mac.as_str()) {
            if device.client_count.is_none() {
                device.client_count = legacy_device
                    .num_sta
                    .and_then(|count| count.try_into().ok());
            }
            if device.wan_ipv6.is_none() {
                device.wan_ipv6 = parse_session_device_wan_ipv6(&legacy_device.extra);
            }
            if device.ports.is_empty()
                || device.radios.is_empty()
                || device.uplink_device_mac.is_none()
                || device.uplink_port_idx.is_none()
            {
                let session_dev: Device = Device::from((*legacy_device).clone());
                if device.ports.is_empty() && !session_dev.ports.is_empty() {
                    device.ports = session_dev.ports;
                }
                if device.radios.is_empty() && !session_dev.radios.is_empty() {
                    device.radios = session_dev.radios;
                }
                if device.uplink_device_mac.is_none() {
                    device.uplink_device_mac = session_dev.uplink_device_mac;
                }
                if device.uplink_port_idx.is_none() {
                    device.uplink_port_idx = session_dev.uplink_port_idx;
                }
            }
        }
    }
}

fn merge_client_traffic(clients: &mut [Client], session_clients: &[SessionClientEntry]) {
    if session_clients.is_empty() {
        return;
    }

    let session_by_ip: HashMap<&str, &SessionClientEntry> = session_clients
        .iter()
        .filter_map(|client| client.ip.as_deref().map(|ip| (ip, client)))
        .collect();
    let mut merged = 0u32;
    for client in clients.iter_mut() {
        let ip_key = client.ip.map(|ip| ip.to_string());
        if let Some(session_client) = ip_key.as_deref().and_then(|ip| session_by_ip.get(ip)) {
            if client.tx_bytes.is_none() {
                client.tx_bytes = session_client
                    .tx_bytes
                    .and_then(|bytes| u64::try_from(bytes).ok());
            }
            if client.rx_bytes.is_none() {
                client.rx_bytes = session_client
                    .rx_bytes
                    .and_then(|bytes| u64::try_from(bytes).ok());
            }
            if client.hostname.is_none() {
                client.hostname.clone_from(&session_client.hostname);
            }
            if client.wireless.is_none() {
                let session_client: Client = Client::from((*session_client).clone());
                client.wireless = session_client.wireless;
            }
            let session_is_wired = session_client.is_wired.unwrap_or(false);
            if client.uplink_device_mac.is_none() {
                let uplink = if session_is_wired {
                    session_client.sw_mac.as_deref()
                } else {
                    session_client.ap_mac.as_deref()
                };
                client.uplink_device_mac = uplink.map(MacAddress::new);
            }
            if client.switch_port.is_none() && session_is_wired {
                client.switch_port = session_client.sw_port.and_then(|p| u32::try_from(p).ok());
            }
            merged += 1;
        }
    }
    tracing::debug!(
        total_clients = clients.len(),
        legacy_available = session_by_ip.len(),
        merged,
        "client traffic merge (by IP)"
    );
}

fn merge_user_reservations(
    clients: &mut [Client],
    session_clients: &[SessionClientEntry],
    session_users: &[SessionUserEntry],
) {
    if session_users.is_empty() {
        return;
    }

    let users_by_mac: HashMap<String, &SessionUserEntry> = session_users
        .iter()
        .map(|user| (user.mac.to_lowercase(), user))
        .collect();
    let mut merged_users = 0u32;
    for client in clients {
        let user = users_by_mac
            .get(&client.mac.as_str().to_lowercase())
            .or_else(|| {
                let ip_str = client.ip.map(|ip| ip.to_string())?;
                let session_client = session_clients
                    .iter()
                    .find(|lc| lc.ip.as_deref() == Some(ip_str.as_str()))?;
                users_by_mac.get(&session_client.mac.to_lowercase())
            });
        if let Some(user) = user {
            client.use_fixedip = user.use_fixedip.unwrap_or(false);
            client.fixed_ip = user.fixed_ip.as_deref().and_then(|ip| ip.parse().ok());
            if client.use_fixedip {
                merged_users += 1;
            }
        }
    }
    tracing::debug!(
        users_available = users_by_mac.len(),
        merged_users,
        "user DHCP reservation merge"
    );
}