tasmor_lib 0.10.0

Rust library to control Tasmota devices via MQTT and HTTP
Documentation
// SPDX-License-Identifier: MPL-2.0
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Subscribable trait for devices that support event subscriptions.
//!
//! This trait is implemented only for MQTT devices. HTTP devices do not
//! support subscriptions because HTTP is a stateless protocol without
//! persistent connections.

use crate::state::{DeviceState, StateChange};
use crate::subscription::{EnergyData, SubscriptionId};
use crate::types::{ColorTemperature, Dimmer, HsbColor, PowerState, Scheme};

/// Trait for types that support event subscriptions.
///
/// This trait provides methods to subscribe to various device events.
/// It is implemented for [`Device<SharedMqttClient>`](crate::Device) but **not** for
/// [`Device<HttpClient>`](crate::Device). Calling subscription methods on an HTTP device
/// is a compile-time error.
///
/// # Type Safety
///
/// ```ignore
/// // MQTT devices support subscriptions
/// let mqtt_device: Device<SharedMqttClient> = ...;
/// mqtt_device.on_power_changed(|idx, state| { /* ... */ }); // OK
///
/// // HTTP devices do NOT support subscriptions — compile-time error:
/// let http_device: Device<HttpClient> = ...;
/// http_device.on_power_changed(|idx, state| { /* ... */ }); // error[E0277]
/// ```
///
/// # Examples
///
/// ```no_run
/// # #[cfg(feature = "mqtt")]
/// use tasmor_lib::MqttBroker;
/// # #[cfg(feature = "mqtt")]
/// use tasmor_lib::subscription::Subscribable;
///
/// # #[cfg(feature = "mqtt")]
/// # async fn example() -> tasmor_lib::Result<()> {
/// let broker = MqttBroker::builder()
///     .host("192.168.1.50")
///     .build()
///     .await?;
///
/// let (device, _) = broker.device("tasmota_device")
///     .build()
///     .await?;
///
/// // Subscribe to power state changes
/// let sub_id = device.on_power_changed(|index, state| {
///     println!("Relay {index} is now {:?}", state);
/// });
///
/// // Subscribe to dimmer changes
/// device.on_dimmer_changed(|dimmer: tasmor_lib::Dimmer| {
///     println!("Brightness: {}%", dimmer.value());
/// });
///
/// // Unsubscribe when no longer needed
/// device.unsubscribe(sub_id);
/// # Ok(())
/// # }
/// ```
pub trait Subscribable {
    /// Subscribes to power state changes.
    ///
    /// The callback is called whenever a relay's power state changes.
    /// It receives the relay index (1-8) and the new power state.
    fn on_power_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(u8, PowerState) + Send + Sync + 'static;

    /// Subscribes to dimmer value changes.
    ///
    /// The callback is called whenever the dimmer level changes.
    fn on_dimmer_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(Dimmer) + Send + Sync + 'static;

    /// Subscribes to HSB color changes.
    ///
    /// The callback is called whenever the device's color changes.
    fn on_color_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(HsbColor) + Send + Sync + 'static;

    /// Subscribes to color temperature changes.
    ///
    /// The callback is called whenever the white color temperature changes.
    fn on_color_temp_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(ColorTemperature) + Send + Sync + 'static;

    /// Subscribes to scheme changes.
    ///
    /// The callback is called whenever the light scheme/effect changes.
    fn on_scheme_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(Scheme) + Send + Sync + 'static;

    /// Subscribes to energy monitoring changes.
    ///
    /// The callback is called whenever energy data changes.
    fn on_energy_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(EnergyData) + Send + Sync + 'static;

    /// Subscribes to connection events.
    ///
    /// The callback is called when the device becomes available.
    /// It receives the initial device state.
    fn on_connected<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(&DeviceState) + Send + Sync + 'static;

    /// Subscribes to disconnection events.
    ///
    /// The callback is called when the device becomes unavailable.
    fn on_disconnected<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn() + Send + Sync + 'static;

    /// Subscribes to reconnection events.
    ///
    /// The callback is called when the MQTT broker connection is restored
    /// after a disconnection. Topics are automatically resubscribed before
    /// this callback is triggered.
    ///
    /// Unlike `on_connected`, this callback does not receive a device state
    /// because the library does not cache state across reconnections. Call
    /// [`Device::query_state`](crate::Device::query_state) inside the callback
    /// to refresh the device state if needed.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # #[cfg(feature = "mqtt")]
    /// use tasmor_lib::{MqttBroker, subscription::Subscribable};
    ///
    /// # #[cfg(feature = "mqtt")]
    /// # async fn example() -> tasmor_lib::Result<()> {
    /// let broker = MqttBroker::builder()
    ///     .host("192.168.1.50")
    ///     .build()
    ///     .await?;
    ///
    /// let (device, _) = broker.device("tasmota_device")
    ///     .build()
    ///     .await?;
    ///
    /// let device_clone = device.clone();
    /// device.on_reconnected(move || {
    ///     let d = device_clone.clone();
    ///     tokio::spawn(async move {
    ///         if let Err(e) = d.query_state().await {
    ///             eprintln!("Failed to refresh state after reconnect: {e}");
    ///         }
    ///     });
    /// });
    /// # Ok(())
    /// # }
    /// ```
    fn on_reconnected<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn() + Send + Sync + 'static;

    /// Subscribes to all state changes.
    ///
    /// This is useful for logging or when you need to react to any change.
    /// The callback receives every state change.
    fn on_state_changed<F>(&self, callback: F) -> SubscriptionId
    where
        F: Fn(&StateChange) + Send + Sync + 'static;

    /// Unsubscribes a callback by its subscription ID.
    ///
    /// Returns `true` if the subscription was found and removed.
    fn unsubscribe(&self, id: SubscriptionId) -> bool;
}