use streamdeck_oxide::{
generic_array::ArrayLength,
md_icons,
plugins::{Plugin, PluginContext, PluginNavigation},
view::customizable::{ClickButton, CustomizableView, ToggleButton},
View,
};
use crate::hass::PersistentHassConnection;
#[derive(Clone)]
pub struct RgbControllerPlugin<W: ArrayLength, H: ArrayLength> {
pub(crate) back_navigation: Option<PluginNavigation<W, H>>,
pub(crate) entity_id: String,
}
const COLORS: &[(&str, (u8, u8, u8))] = &[
("Red", (255, 0, 0)),
("Green", (0, 255, 0)),
("Blue", (0, 0, 255)),
("White", (255, 255, 255)),
("Warm White", (255, 200, 100)),
("Cool White", (200, 220, 255)),
("Purple", (128, 0, 128)),
("Yellow", (255, 255, 0)),
("Cyan", (0, 255, 255)),
("Orange", (255, 165, 0)),
("Pink", (255, 192, 203)),
("Teal", (0, 128, 128)),
];
fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
let r_f = r as f32 / 255.0;
let g_f = g as f32 / 255.0;
let b_f = b as f32 / 255.0;
let max = r_f.max(g_f).max(b_f);
let min = r_f.min(g_f).min(b_f);
let delta = max - min;
let h = if delta == 0.0 {
0.0
} else if max == r_f {
60.0 * (((g_f - b_f) / delta) % 6.0)
} else if max == g_f {
60.0 * (((b_f - r_f) / delta) + 2.0)
} else {
60.0 * (((r_f - g_f) / delta) + 4.0)
};
let s = if max == 0.0 { 0.0 } else { delta / max };
let v = max;
(h, s * 100.0, v * 100.0)
}
#[async_trait::async_trait]
impl<W, H> Plugin<W, H> for RgbControllerPlugin<W, H>
where
W: ArrayLength,
H: ArrayLength,
{
fn name(&self) -> &'static str {
"RgbControllerPlugin"
}
async fn get_view(
&self,
_context: PluginContext,
) -> Result<
Box<dyn View<W, H, PluginContext, PluginNavigation<W, H>>>,
Box<dyn std::error::Error>,
> {
let mut view = CustomizableView::new();
let entity_id = self.entity_id.clone();
let entity_id_2 = entity_id.clone();
view.set_button(
0,
0,
ToggleButton::new(
"On/Off",
Some(md_icons::filled::ICON_TOGGLE_OFF),
move |ctx: PluginContext| {
let entity_id = entity_id.clone();
async move {
let hass = ctx
.get_context::<PersistentHassConnection>()
.await
.ok_or("Failed to get PersistentHassConnection")?;
let state = hass
.get_state(&entity_id)
.await
.ok_or("Failed to get state")?;
Ok(state.state == "on")
}
},
move |ctx, value| {
let entity_id = entity_id_2.clone();
async move {
let hass = ctx
.get_context::<PersistentHassConnection>()
.await
.ok_or("Failed to get PersistentHassConnection")?;
hass.call_service(
"light",
if value { "turn_on" } else { "turn_off" },
Some(serde_json::json!({ "entity_id": entity_id })),
)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
},
)
.when_active("On/Off", Some(md_icons::filled::ICON_TOGGLE_ON)),
)?;
let max_buttons = W::to_usize() * H::to_usize() - 2; let colors_to_show = std::cmp::min(COLORS.len(), max_buttons);
for (index, &(color_name, (r, g, b))) in COLORS.iter().take(colors_to_show).enumerate() {
let button_index = index + 1; let x = button_index % W::to_usize();
let y = button_index / W::to_usize();
let entity_id = self.entity_id.clone();
let (h, s, v) = rgb_to_hsv(r, g, b);
view.set_button(
x,
y,
ClickButton::new(
color_name,
None, move |ctx: PluginContext| {
let entity_id = entity_id.clone();
let h = h;
let s = s;
let v = v;
async move {
let hass = ctx
.get_context::<PersistentHassConnection>()
.await
.ok_or("Failed to get PersistentHassConnection")?;
hass.call_service(
"light",
"turn_on",
Some(serde_json::json!({
"entity_id": entity_id,
"hs_color": [h, s],
"brightness_pct": v
})),
)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
},
)
)?;
}
if let Some(back_navigation) = &self.back_navigation {
view.set_navigation(
W::to_usize() - 1,
H::to_usize() - 1,
back_navigation.clone(),
"Back",
Some(md_icons::filled::ICON_ARROW_BACK),
)?;
}
Ok(Box::new(view))
}
}