pub mod cloud;
pub mod local;
#[cfg(test)]
pub(crate) mod mock;
use async_trait::async_trait;
use crate::error::Result;
use crate::types::{
BackendType, Color, Device, DeviceId, DeviceState, DiyScene, LightScene, WorkMode,
};
#[async_trait]
pub trait GoveeBackend: Send + Sync {
async fn list_devices(&self) -> Result<Vec<Device>>;
async fn get_state(&self, id: &DeviceId) -> Result<DeviceState>;
async fn set_power(&self, id: &DeviceId, on: bool) -> Result<()>;
async fn set_brightness(&self, id: &DeviceId, value: u8) -> Result<()>;
async fn set_color(&self, id: &DeviceId, color: Color) -> Result<()>;
async fn set_color_temp(&self, id: &DeviceId, kelvin: u32) -> Result<()>;
async fn list_scenes(&self, id: &DeviceId) -> Result<Vec<LightScene>>;
async fn set_scene(&self, id: &DeviceId, scene: &LightScene) -> Result<()>;
async fn list_diy_scenes(&self, id: &DeviceId) -> Result<Vec<DiyScene>>;
async fn set_diy_scene(&self, id: &DeviceId, scene: &DiyScene) -> Result<()>;
async fn set_segment_color(&self, id: &DeviceId, segments: &[u8], color: Color) -> Result<()>;
async fn set_segment_brightness(
&self,
id: &DeviceId,
segments: &[u8],
brightness: u8,
) -> Result<()>;
async fn list_work_modes(&self, id: &DeviceId) -> Result<Vec<WorkMode>>;
async fn set_work_mode(
&self,
id: &DeviceId,
work_mode: u32,
mode_value: Option<u32>,
) -> Result<()>;
fn backend_type(&self) -> BackendType;
}
#[cfg(test)]
mod tests {
use super::mock::MockBackend;
use super::*;
fn _assert_send_sync<T: Send + Sync>() {}
fn _assert_object_safe(_: &dyn GoveeBackend) {}
#[test]
fn trait_is_send_sync() {
_assert_send_sync::<MockBackend>();
}
#[tokio::test]
async fn mock_list_devices_empty() {
let mock = MockBackend::new();
let devices = mock.list_devices().await.unwrap();
assert!(devices.is_empty());
}
#[tokio::test]
async fn mock_list_devices_with_entries() {
let device = Device {
id: DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap(),
model: "H6076".into(),
name: "Test Light".into(),
alias: None,
backend: BackendType::Cloud,
};
let mock = MockBackend::new().with_devices(vec![device.clone()]);
let devices = mock.list_devices().await.unwrap();
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].id, device.id);
}
#[tokio::test]
async fn mock_get_state_returns_configured() {
let state = DeviceState::new(
true,
75,
Color::new(255, 0, 0),
None,
false,
std::collections::HashMap::new(),
)
.unwrap();
let mock = MockBackend::new().with_state(state);
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let result = mock.get_state(&id).await.unwrap();
assert_eq!(result.brightness, 75);
assert!(result.on);
}
#[tokio::test]
async fn mock_get_state_not_found() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let result = mock.get_state(&id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn mock_set_operations_succeed() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
assert!(mock.set_power(&id, true).await.is_ok());
assert!(mock.set_brightness(&id, 50).await.is_ok());
assert!(mock.set_color(&id, Color::new(0, 255, 0)).await.is_ok());
assert!(mock.set_color_temp(&id, 4000).await.is_ok());
}
#[tokio::test]
async fn mock_list_scenes_returns_empty() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let scenes = mock.list_scenes(&id).await.unwrap();
assert!(scenes.is_empty());
}
#[tokio::test]
async fn mock_set_scene_succeeds() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let scene = LightScene {
id: 1,
name: "Sunrise".into(),
param_id: 100,
};
assert!(mock.set_scene(&id, &scene).await.is_ok());
}
#[tokio::test]
async fn mock_list_diy_scenes_returns_empty() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let scenes = mock.list_diy_scenes(&id).await.unwrap();
assert!(scenes.is_empty());
}
#[tokio::test]
async fn mock_set_diy_scene_succeeds() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let scene = crate::types::DiyScene {
id: 42,
name: Some("Custom".into()),
};
assert!(mock.set_diy_scene(&id, &scene).await.is_ok());
}
#[tokio::test]
async fn mock_set_segment_operations_succeed() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
assert!(
mock.set_segment_color(&id, &[0, 1], Color::new(255, 0, 0))
.await
.is_ok()
);
assert!(mock.set_segment_brightness(&id, &[0, 1], 80).await.is_ok());
}
#[tokio::test]
async fn mock_list_work_modes_returns_empty() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
let modes = mock.list_work_modes(&id).await.unwrap();
assert!(modes.is_empty());
}
#[tokio::test]
async fn mock_set_work_mode_succeeds() {
let mock = MockBackend::new();
let id = DeviceId::new("AA:BB:CC:DD:EE:FF").unwrap();
assert!(mock.set_work_mode(&id, 1, Some(3)).await.is_ok());
assert!(mock.set_work_mode(&id, 2, None).await.is_ok());
}
#[test]
fn mock_backend_type_default_cloud() {
let mock = MockBackend::new();
assert_eq!(mock.backend_type(), BackendType::Cloud);
}
#[test]
fn mock_backend_type_configurable() {
let mock = MockBackend::new().with_backend_type(BackendType::Local);
assert_eq!(mock.backend_type(), BackendType::Local);
}
#[tokio::test]
async fn trait_object_dispatch() {
let mock = MockBackend::new();
let backend: Box<dyn GoveeBackend> = Box::new(mock);
let devices = backend.list_devices().await.unwrap();
assert!(devices.is_empty());
assert_eq!(backend.backend_type(), BackendType::Cloud);
}
}