Files
ambiligth/src/homeassistant.rs
2026-03-16 14:25:32 +01:00

190 lines
5.4 KiB
Rust

use reqwest::{Client, Error};
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Clone)]
pub struct HaClient {
base_url: String,
token: String,
client: Client,
}
#[derive(Debug, Deserialize)]
pub struct HaState {
pub entity_id: String,
pub attributes: serde_json::Value,
}
impl HaClient {
pub fn new(base_url: &str, token: &str) -> Self {
Self {
base_url: base_url.trim_end_matches('/').to_string(),
token: token.to_string(),
client: Client::new(),
}
}
pub async fn get_lights(&self) -> Result<Vec<HaState>, Error> {
let url = format!("{}/api/states", self.base_url);
let states: Vec<HaState> = self
.client
.get(&url)
.bearer_auth(&self.token)
.send()
.await?
.json()
.await?;
Ok(states
.into_iter()
.filter(|s| s.entity_id.starts_with("light."))
.collect())
}
pub async fn get_color_lights(&self) -> Result<Vec<HaState>, Error> {
let color_modes = ["hs", "xy", "rgb", "rgbw", "rgbww"];
let lights = self.get_lights().await?;
Ok(lights
.into_iter()
.filter(|l| {
l.attributes
.get("supported_color_modes")
.and_then(|m| m.as_array())
.map(|modes| {
modes
.iter()
.any(|m| m.as_str().map_or(false, |s| color_modes.contains(&s)))
})
.unwrap_or(false)
})
.collect())
}
pub async fn create_scene_snapshot(
&self,
scene_id: &str,
entities: &[String],
) -> Result<(), Error> {
self.client
.post(format!("{}/api/services/scene/create", self.base_url))
.bearer_auth(&self.token)
.json(&json!({
"scene_id": scene_id,
"snapshot_entities": entities,
}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn turn_on_scene(&self, scene_entity_id: &str) -> Result<(), Error> {
self.client
.post(format!("{}/api/services/scene/turn_on", self.base_url))
.bearer_auth(&self.token)
.json(&json!({ "entity_id": scene_entity_id }))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn set_light_color(
&self,
entity_id: &str,
r: u8,
g: u8,
b: u8,
states: &[HaState],
) -> Result<(), Error> {
let url = format!("{}/api/services/light/turn_on", self.base_url);
let mut payload = json!({
"entity_id": entity_id,
});
if let Some(state) = states.iter().find(|s| s.entity_id == entity_id) {
if let Some(brightness) = state.attributes.get("brightness") {
payload["brightness"] = brightness.clone();
}
let color_mode = state
.attributes
.get("color_mode")
.and_then(|m| m.as_str())
.unwrap_or("");
match color_mode {
"rgbw" => {
let w = state
.attributes
.get("rgbw_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
payload["rgbw_color"] = json!([r, g, b, w]);
}
"rgbww" => {
let cw = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(3))
.cloned()
.unwrap_or(json!(0));
let ww = state
.attributes
.get("rgbww_color")
.and_then(|v| v.as_array())
.and_then(|a| a.get(4))
.cloned()
.unwrap_or(json!(0));
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
}
_ => {
payload["rgb_color"] = json!([r, g, b]);
}
}
} else {
payload["rgb_color"] = json!([r, g, b]);
}
self.client
.post(&url)
.bearer_auth(&self.token)
.json(&payload)
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn set_lights_color_parallel(
&self,
entity_ids: &[String],
r: u8,
g: u8,
b: u8,
) -> Result<(), Error> {
let states = self.get_lights().await.unwrap_or_default();
let futures: Vec<_> = entity_ids
.iter()
.map(|entity_id| self.set_light_color(entity_id, r, g, b, &states))
.collect();
let results = futures::future::join_all(futures).await;
for result in results {
if let Err(e) = result {
eprintln!("Failed to update light: {}", e);
}
}
Ok(())
}
}