shelly-core 0.2.2

Core Shelly device API and model types (Gen1/Gen2/Gen3 HTTP + RPC clients)
Documentation
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;

use ipnet::Ipv4Net;
use tokio::sync::mpsc;
use tokio::time::timeout;

use crate::Result;
use crate::model::DeviceInfo;

use super::probe_device;

pub async fn scan_subnet(
    subnet: Ipv4Net,
    http_timeout: Duration,
    show_progress: bool,
    on_found: impl Fn(&DeviceInfo),
) -> Result<Vec<DeviceInfo>> {
    let client = reqwest::Client::builder().timeout(http_timeout).build()?;

    let (tx, mut rx) = mpsc::channel::<DeviceInfo>(64);

    let hosts: Vec<Ipv4Addr> = subnet.hosts().collect();
    let total = hosts.len() as u32;
    let completed = Arc::new(AtomicU32::new(0));

    for chunk in hosts.chunks(32) {
        let mut handles = Vec::new();
        for &ip in chunk {
            let client = client.clone();
            let tx = tx.clone();
            let completed = Arc::clone(&completed);
            handles.push(tokio::spawn(async move {
                let addr = IpAddr::V4(ip);
                if let Ok(Ok(info)) = timeout(http_timeout, probe_device(addr, &client)).await {
                    let _ = tx.send(info).await;
                }
                completed.fetch_add(1, Ordering::Relaxed);
            }));
        }

        for handle in handles {
            let _ = handle.await;
        }

        if show_progress {
            let done = completed.load(Ordering::Relaxed);
            eprint!("\rScanning {subnet}... {done}/{total}");
        }
    }

    if show_progress {
        // Clear the progress line
        eprint!("\r{}\r", " ".repeat(60));
    }

    drop(tx);

    let mut devices = Vec::new();
    while let Some(info) = rx.recv().await {
        on_found(&info);
        devices.push(info);
    }

    devices.sort_by_key(|a| a.ip);
    Ok(devices)
}

/// Enrich Gen1 devices with their name from /settings
pub async fn enrich_gen1_name(info: &mut DeviceInfo, client: &reqwest::Client) -> Result<()> {
    if info.name.is_some() {
        return Ok(());
    }

    let url = format!("http://{}/settings", info.ip);
    let resp = client.get(&url).send().await?;

    let status = resp.status();
    if !status.is_success() {
        let body = resp.text().await.unwrap_or_default();
        return Err(crate::error::status_error(status, &url, &body));
    }

    let settings: serde_json::Value = resp.json().await?;

    if let Some(name) = settings.get("name").and_then(|v| v.as_str())
        && !name.is_empty()
    {
        info.name = Some(name.to_string());
    }

    Ok(())
}