use anyhow::Result;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DeviceResponse {
pub code: i32,
pub message: String,
pub data: Option<Vec<Device>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Device {
pub sku: String,
pub device: String,
#[serde(rename = "deviceName")]
pub device_name: String,
pub capabilities: Vec<Capability>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Capability {
#[serde(rename = "type")]
pub capability_type: String,
pub instance: String,
pub parameters: Option<serde_json::Value>,
}
#[derive(Clone)]
pub struct GoveeClient {
client: reqwest::Client,
api_key: String,
base_url: String,
devices: Arc<RwLock<Vec<Device>>>,
}
impl GoveeClient {
pub fn new(api_key: String) -> Self {
let client = reqwest::Client::new();
Self {
client,
api_key,
base_url: "https://openapi.api.govee.com".to_string(),
devices: Arc::new(RwLock::new(Vec::new())),
}
}
fn build_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(
"Govee-API-Key",
HeaderValue::from_str(&self.api_key).expect("Invalid API key format"),
);
headers
}
pub async fn fetch_devices(&self) -> Result<Vec<Device>> {
let url = format!("{}/router/api/v1/user/devices", self.base_url);
log::debug!("Fetching devices from: {}", url);
let response = self
.client
.get(&url)
.headers(self.build_headers())
.send()
.await?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("Failed to fetch devices: {}", status);
}
let body = response.text().await?;
let device_response: DeviceResponse = serde_json::from_str(&body)?;
if let Some(devices) = device_response.data {
let mut device_cache = self.devices.write().await;
*device_cache = devices.clone();
log::info!("Found {} devices", devices.len());
Ok(devices)
} else {
Ok(Vec::new())
}
}
pub async fn get_cached_devices(&self) -> Vec<Device> {
self.devices.read().await.clone()
}
pub async fn control_device(
&self,
device_id: &str,
sku: &str,
capability: &str,
instance: &str,
value: serde_json::Value,
) -> Result<serde_json::Value> {
let url = format!("{}/router/api/v1/device/control", self.base_url);
let request_body = serde_json::json!({
"requestId": uuid::Uuid::new_v4().to_string(),
"payload": {
"sku": sku,
"device": device_id,
"capability": {
"type": capability,
"instance": instance,
"value": value
}
}
});
log::debug!(
"Controlling device: {} with {}/{} = {:?}",
device_id,
capability,
instance,
value
);
let response = self
.client
.post(&url)
.headers(self.build_headers())
.json(&request_body)
.send()
.await?;
let status = response.status();
let body = response.text().await?;
if !status.is_success() {
anyhow::bail!("Control failed: {} - {}", status, body);
}
let result: serde_json::Value = serde_json::from_str(&body)?;
match result.get("code").and_then(|v| v.as_i64()) {
Some(200) | None => {} Some(code) => {
log::error!("Govee API returned code {}: {}", code, body);
anyhow::bail!("Govee API error code {}", code);
}
}
let is_brightness_range =
capability == "devices.capabilities.range" && instance == "brightness";
if is_brightness_range {
match value.as_u64() {
Some(val) if !(1..=100).contains(&val) => {
log::warn!(
"API accepted out-of-range brightness value {} without error",
val
);
}
_ => {}
}
}
Ok(result)
}
pub async fn set_power(&self, device_id: &str, sku: &str, on: bool) -> Result<()> {
self.control_device(
device_id,
sku,
"devices.capabilities.on_off",
"powerSwitch",
serde_json::json!(if on { 1 } else { 0 }),
)
.await?;
Ok(())
}
pub async fn set_brightness(&self, device_id: &str, sku: &str, brightness: u32) -> Result<()> {
let valid_brightness = if brightness < 1 {
log::warn!(
"Brightness {} is below minimum (1), setting to 1",
brightness
);
1
} else if brightness > 100 {
log::warn!(
"Brightness {} is above maximum (100), setting to 100",
brightness
);
100
} else {
brightness
};
self.control_device(
device_id,
sku,
"devices.capabilities.range",
"brightness",
serde_json::json!(valid_brightness),
)
.await?;
Ok(())
}
pub async fn set_color_rgb(
&self,
device_id: &str,
sku: &str,
r: u8,
g: u8,
b: u8,
) -> Result<()> {
let rgb_value = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
log::debug!(
"Setting color RGB: r={}, g={}, b={} -> value={} (0x{:06X})",
r,
g,
b,
rgb_value,
rgb_value
);
let result = self
.control_device(
device_id,
sku,
"devices.capabilities.color_setting",
"colorRgb",
serde_json::json!(rgb_value),
)
.await?;
log::debug!("Color command result: {:?}", result);
Ok(())
}
pub async fn set_color_temperature(
&self,
device_id: &str,
sku: &str,
temperature: u32,
) -> Result<()> {
let valid_temperature = if temperature < 2200 {
log::warn!(
"Color temperature {}K is below minimum (2200K), setting to 2200K",
temperature
);
2200
} else if temperature > 6500 {
log::warn!(
"Color temperature {}K is above maximum (6500K), setting to 6500K",
temperature
);
6500
} else {
temperature
};
self.control_device(
device_id,
sku,
"devices.capabilities.color_setting",
"colorTemperatureK",
serde_json::json!(valid_temperature),
)
.await?;
Ok(())
}
}