Library (unifly-api)

Rust library for custom UniFi integrations

Crates.io docs.rs

The engine behind unifly is published independently on crates.io. Use it to build your own UniFi tools, integrations, or automations in Rust.

๐Ÿ”—Quick Start

Add to your Cargo.toml:

[dependencies]
unifly-api = "0.8"
secrecy = "0.10"
tokio = { version = "1", features = ["full"] }

๐Ÿ”—Low-Level API Access

Talk directly to the controller with the IntegrationClient:

use unifly_api::{IntegrationClient, TransportConfig, TlsMode, ControllerPlatform};
use secrecy::SecretString;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let transport = TransportConfig {
        tls: TlsMode::DangerAcceptInvalid,
        ..Default::default()
    };
    let client = IntegrationClient::from_api_key(
        "https://192.168.1.1",
        &SecretString::from("your-api-key"),
        &transport,
        ControllerPlatform::UnifiOs,
    )?;

    // list_devices takes (site_uuid, offset, limit)
    let site_id = uuid::Uuid::parse_str("your-site-uuid")?;
    let page = client.list_devices(&site_id, 0, 50).await?;
    println!("Found {} devices", page.data.len());
    Ok(())
}

The IntegrationClient gives you direct control over individual API calls. Use it when you need to target specific endpoints or build custom query patterns.

For Session API access (events, stats, device commands), use SessionClient with cookie/CSRF auth instead.

๐Ÿ”—High-Level Controller

The Controller manages both APIs, background refresh, WebSocket events, and data merging:

use unifly_api::{Controller, ControllerConfig, AuthCredentials, TlsVerification};
use secrecy::SecretString;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ControllerConfig {
        url: "https://192.168.1.1".parse()?,
        auth: AuthCredentials::ApiKey(SecretString::from("your-api-key")),
        tls: TlsVerification::DangerAcceptInvalid,
        ..Default::default()
    };
    let controller = Controller::new(config);
    controller.connect().await?;

    // Snapshot: get current data immediately
    let devices = controller.devices_snapshot();
    println!("Found {} devices", devices.len());

    // Reactive subscription: notified when data changes
    let mut stream = controller.devices();
    while let Some(updated) = stream.changed().await {
        println!("Device count updated: {}", updated.len());
    }

    Ok(())
}

๐Ÿ”—When to Use Which

ApproachUse Case
IntegrationClientDirect REST calls, custom query patterns, Integration API only
SessionClientEvents, stats, device commands, Session API only
ControllerFull lifecycle with both APIs, automatic refresh, reactive subscriptions
Controller::oneshot()Single CLI-style fetch with no background tasks

๐Ÿ”—Architecture

graph TD
subgraph "unifly-api"
IC["IntegrationClient
REST + API Key"] SC["SessionClient
Cookie + CSRF"] WS["WebSocket
Live Events"] CTRL["Controller
Lifecycle + Routing"] DS["DataStore
DashMap + watch"] end CTRL --> IC CTRL --> SC CTRL --> WS IC --> DS SC --> DS WS --> DS DS --> ES["EntityStream<T>
Reactive subscriptions"]
TypePurpose
ControllerMain entry point. Wraps Arc<ControllerInner> for cheap cloning across async tasks
DataStoreEntity storage. DashMap + watch channels for lock-free reactive updates
EntityStream<T>Reactive subscription. current() for a snapshot, changed() to await the next update (returns None when the controller disconnects)
EntityIdDual-identity enum: Uuid(Uuid) for Integration API or Legacy(String) for Session API
AuthCredentialsAuth mode: ApiKey, Credentials, Hybrid, or Cloud variants

๐Ÿ”—Connection Modes

ModeUse CaseBackground Tasks
Controller::connect()Long-lived apps (TUI, daemons)Refresh loop (10s), WebSocket, command processor
Controller::oneshot()Fire-and-forget (CLI commands)None. Single fetch, then done

๐Ÿ”—Full Documentation

See docs.rs/unifly-api for the complete API reference with all types, methods, and examples.

๐Ÿ”—Next Steps