rship-govee 0.1.1

rship executor for controlling Govee smart home devices
Documentation
use anyhow::Result;
use rship_sdk::{ActionArgs, InstanceArgs, SdkClient, TargetArgs};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time::{Duration, interval};

use crate::actions::{BrightnessAction, ColorAction, ColorTemperatureAction, PowerAction};
use crate::client::{Device, GoveeClient};

#[derive(Debug)]
pub enum GoveeCommand {
    Power {
        device_id: String,
        sku: String,
        on: bool,
    },
    Brightness {
        device_id: String,
        sku: String,
        brightness: u32,
    },
    Color {
        device_id: String,
        sku: String,
        r: u8,
        g: u8,
        b: u8,
    },
    ColorTemperature {
        device_id: String,
        sku: String,
        temperature: u32,
    },
}

pub struct GoveeService {
    sdk_client: SdkClient,
    govee_client: Arc<GoveeClient>,
    rship_address: String,
    rship_port: u16,
}

impl GoveeService {
    pub async fn new(api_key: String, rship_address: String, rship_port: u16) -> Result<Self> {
        let sdk_client = SdkClient::init();
        let govee_client = Arc::new(GoveeClient::new(api_key));

        Ok(Self {
            sdk_client,
            govee_client,
            rship_address,
            rship_port,
        })
    }

    pub async fn start(&self) -> Result<()> {
        log::info!("Starting Govee integration service");

        // Connect to rship
        self.setup_rship_connection().await?;

        // Fetch devices from Govee
        let devices = self.govee_client.fetch_devices().await?;

        // Create command channel
        let (command_tx, command_rx) = mpsc::channel::<GoveeCommand>(100);

        // Setup rship instance with targets for each device
        self.setup_rship_instance(devices, command_tx).await?;

        // Start command handler task
        self.start_command_handler(command_rx).await?;

        // Start periodic device refresh
        self.start_device_refresh_task().await?;

        log::info!("Service started successfully");
        std::future::pending::<()>().await;

        Ok(())
    }

    async fn setup_rship_connection(&self) -> Result<()> {
        let url = format!("ws://{}:{}/myko", self.rship_address, self.rship_port);
        log::info!("Connecting to rship at: {}", url);

        self.sdk_client.set_address(Some(url));
        self.sdk_client.await_connection().await;

        log::info!("Connected to rship successfully");
        Ok(())
    }

    async fn setup_rship_instance(
        &self,
        devices: Vec<Device>,
        command_tx: mpsc::Sender<GoveeCommand>,
    ) -> Result<()> {
        // Create main instance
        let instance = self
            .sdk_client
            .add_instance(InstanceArgs {
                name: "Govee Smart Home".into(),
                short_id: "govee".into(),
                code: "govee".into(),
                service_id: "govee-service".into(),
                cluster_id: None,
                color: "#00D9FF".into(),
                machine_id: format!("govee-{}", std::process::id()),
                message: Some("Govee Smart Home Controller".into()),
                status: rship_sdk::InstanceStatus::Available,
            })
            .await;

        // Create a target for each device
        for device in devices {
            let device_name = format!("{} ({})", device.device_name, &device.device[12..]);
            let device_id = device.device.clone();
            let sku = device.sku.clone();

            let mut target = instance
                .add_target(TargetArgs {
                    name: device_name.clone(),
                    short_id: device_id.clone(),
                    category: "light".into(),
                    parent_targets: None,
                })
                .await;

            // Add Power action
            let tx_power = command_tx.clone();
            let device_id_power = device_id.clone();
            let sku_power = sku.clone();
            target
                .add_action(
                    ActionArgs::<PowerAction>::new("Power".into(), "power".into()),
                    move |_action, data| {
                        let tx = tx_power.clone();
                        let device_id = device_id_power.clone();
                        let sku = sku_power.clone();
                        tokio::spawn(async move {
                            if let Err(e) = tx
                                .send(GoveeCommand::Power {
                                    device_id,
                                    sku,
                                    on: data.state,
                                })
                                .await
                            {
                                log::error!("Failed to send power command: {}", e);
                            }
                        });
                    },
                )
                .await;

            // Add Brightness action if device supports it
            if device
                .capabilities
                .iter()
                .any(|c| c.instance == "brightness")
            {
                let tx_brightness = command_tx.clone();
                let device_id_brightness = device_id.clone();
                let sku_brightness = sku.clone();
                target
                    .add_action(
                        ActionArgs::<BrightnessAction>::new(
                            "Brightness".into(),
                            "brightness".into(),
                        ),
                        move |_action, data| {
                            let tx = tx_brightness.clone();
                            let device_id = device_id_brightness.clone();
                            let sku = sku_brightness.clone();
                            tokio::spawn(async move {
                                if let Err(e) = tx
                                    .send(GoveeCommand::Brightness {
                                        device_id,
                                        sku,
                                        brightness: data.brightness,
                                    })
                                    .await
                                {
                                    log::error!("Failed to send brightness command: {}", e);
                                }
                            });
                        },
                    )
                    .await;
            }

            // Add Color action if device supports it
            if device.capabilities.iter().any(|c| c.instance == "colorRgb") {
                let tx_color = command_tx.clone();
                let device_id_color = device_id.clone();
                let sku_color = sku.clone();
                target
                    .add_action(
                        ActionArgs::<ColorAction>::new("Color".into(), "color".into()),
                        move |_action, data| {
                            let tx = tx_color.clone();
                            let device_id = device_id_color.clone();
                            let sku = sku_color.clone();
                            tokio::spawn(async move {
                                if let Err(e) = tx
                                    .send(GoveeCommand::Color {
                                        device_id,
                                        sku,
                                        r: data.r,
                                        g: data.g,
                                        b: data.b,
                                    })
                                    .await
                                {
                                    log::error!("Failed to send color command: {}", e);
                                }
                            });
                        },
                    )
                    .await;
            }

            // Add Color Temperature action if device supports it
            if device
                .capabilities
                .iter()
                .any(|c| c.instance == "colorTemperatureK")
            {
                let tx_temp = command_tx.clone();
                let device_id_temp = device_id.clone();
                let sku_temp = sku.clone();
                target
                    .add_action(
                        ActionArgs::<ColorTemperatureAction>::new(
                            "Color Temperature".into(),
                            "color-temperature".into(),
                        ),
                        move |_action, data| {
                            let tx = tx_temp.clone();
                            let device_id = device_id_temp.clone();
                            let sku = sku_temp.clone();
                            tokio::spawn(async move {
                                if let Err(e) = tx
                                    .send(GoveeCommand::ColorTemperature {
                                        device_id,
                                        sku,
                                        temperature: data.temperature,
                                    })
                                    .await
                                {
                                    log::error!("Failed to send color temperature command: {}", e);
                                }
                            });
                        },
                    )
                    .await;
            }

            log::info!("Created target for device: {}", device_name);
        }

        Ok(())
    }

    async fn start_command_handler(
        &self,
        mut command_rx: mpsc::Receiver<GoveeCommand>,
    ) -> Result<()> {
        let govee_client = self.govee_client.clone();

        tokio::spawn(async move {
            while let Some(command) = command_rx.recv().await {
                let result = match command {
                    GoveeCommand::Power { device_id, sku, on } => {
                        log::info!(
                            "Power {} → {}",
                            if on { "ON" } else { "OFF" },
                            &device_id[12..]
                        );
                        govee_client.set_power(&device_id, &sku, on).await
                    }
                    GoveeCommand::Brightness {
                        device_id,
                        sku,
                        brightness,
                    } => {
                        log::info!("Brightness {}% → {}", brightness, &device_id[12..]);
                        govee_client
                            .set_brightness(&device_id, &sku, brightness)
                            .await
                    }
                    GoveeCommand::Color {
                        device_id,
                        sku,
                        r,
                        g,
                        b,
                    } => {
                        log::info!("Color RGB({},{},{}) → {}", r, g, b, &device_id[12..]);
                        govee_client.set_color_rgb(&device_id, &sku, r, g, b).await
                    }
                    GoveeCommand::ColorTemperature {
                        device_id,
                        sku,
                        temperature,
                    } => {
                        log::info!("Temperature {}K → {}", temperature, &device_id[12..]);
                        govee_client
                            .set_color_temperature(&device_id, &sku, temperature)
                            .await
                    }
                };

                if let Err(e) = result {
                    log::error!("Command execution failed: {}", e);
                }
            }
        });

        Ok(())
    }

    async fn start_device_refresh_task(&self) -> Result<()> {
        let govee_client = self.govee_client.clone();

        tokio::spawn(async move {
            let mut interval = interval(Duration::from_secs(300)); // Refresh every 5 minutes

            loop {
                interval.tick().await;
                log::debug!("Refreshing device list");

                if let Err(e) = govee_client.fetch_devices().await {
                    log::error!("Failed to refresh devices: {}", e);
                }
            }
        });

        Ok(())
    }
}