switchkit 0.2.0

Vendor-neutral abstraction for smart-plug devices (Shelly, Tasmota).
Documentation
use serde_json::Value;

use crate::error::Result;
use crate::model::{DeviceSnapshot, PowerAction, Relay};
use crate::target::{DeviceTarget, Vendor};

/// A vendor client. ONE implementation IS one vendor and serves MANY devices - methods take
/// a `&DeviceTarget`, so it is a shared, stateless client, NOT a per-device object. I/O methods
/// are async so a caller on an async runtime awaits them directly.
#[async_trait::async_trait]
pub trait SmartDevice: Send + Sync {
    fn vendor(&self) -> Vendor;

    /// Probe a target for THIS vendor's signature. `Ok(Some(snapshot))` = reachable and
    /// CONFIRMED this vendor; `Ok(None)` = reachable but not this vendor's signature; `Err` =
    /// no-answer / needs-auth / could-not-confirm. ONLY `Ok(Some)` classifies a host - for
    /// discovery, `Ok(None)` and `Err` are BOTH "not this vendor, try the next client", so a
    /// core impl that cannot perfectly distinguish reachable-non-match from no-answer (some
    /// vendor APIs conflate an HTTP 4xx with a transport failure) never causes a wrong guess:
    /// a host is only classified when SOME client returns `Ok(Some)`, else it is unrecognized.
    async fn probe(&self, target: &DeviceTarget) -> Result<Option<DeviceSnapshot>>;

    /// One refresh. `Ok` = reachable reading; `Err` = unreadable (the consumer renders offline).
    async fn status(&self, target: &DeviceTarget) -> Result<DeviceSnapshot>;

    /// Switch a relay. `channel` = None targets the sole/first relay. Returns the CONFIRMED
    /// relay state read back from the device (never optimistic).
    async fn set_power(
        &self,
        target: &DeviceTarget,
        channel: Option<u8>,
        action: PowerAction,
    ) -> Result<Relay>;

    async fn firmware_version(&self, target: &DeviceTarget) -> Result<Option<String>>;
    async fn firmware_update(&self, target: &DeviceTarget, ota_url: Option<&str>) -> Result<()>;
    async fn config_get(&self, target: &DeviceTarget, setting: &str) -> Result<Value>;
    async fn config_set(&self, target: &DeviceTarget, setting: &str, value: &str) -> Result<Value>;
    async fn backup(&self, target: &DeviceTarget) -> Result<Vec<u8>>;
    /// A raw vendor command / RPC (Tasmota console, Shelly RPC method). The CALLER classifies
    /// it via `guardrail::classify` first; the core just executes and returns the raw JSON.
    async fn console(&self, target: &DeviceTarget, command: &str) -> Result<Value>;
}

/// A device found during discovery, already classified to a vendor. The host lives on
/// `snapshot.host` - not duplicated here.
#[derive(Debug, Clone)]
pub struct Discovered {
    pub vendor: Vendor,
    pub snapshot: DeviceSnapshot,
}