Skip to main content

tanuki_common/capabilities/
light.rs

1use alloc::{vec, vec::Vec};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Property, property};
6
7pub trait LightProperty: Property {}
8
9#[property(LightProperty, State, key = "state")]
10pub struct LightState {
11    /// Should also be provided by tanuki.on_off
12    pub on: bool,
13    /// Brightness level (0.0-1.0)
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub brightness: Option<f32>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub color: Option<Color>,
18}
19
20#[property(LightProperty, Command, key = "command")]
21pub struct LightCommand {
22    pub on: bool,
23    /// Brightness level (0.0-1.0)
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub brightness: Option<f32>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub color: Option<Color>,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(untagged)]
32#[serde(deny_unknown_fields)]
33pub enum Color {
34    /// Red, green, blue, cool white, warm white, each 0-255
35    Rgbww { r: u8, g: u8, b: u8, cw: u8, ww: u8 },
36    /// Red, green, blue, white, each 0-255
37    Rgbw { r: u8, g: u8, b: u8, w: u8 },
38    /// Red, green, blue, each 0-255
39    Rgb { r: u8, g: u8, b: u8 },
40    /// Hue (0-360), saturation (0-100)
41    Hs { h: f32, s: f32 },
42    /// CIE 1931 color space x,y coordinates (0.0-1.0)
43    Xy { x: f32, y: f32 },
44}
45
46impl Color {
47    /// Convert to Home Assistant color representation
48    pub fn to_hass(&self) -> Vec<f32> {
49        match *self {
50            Color::Rgbww { r, g, b, cw, ww } => {
51                vec![r as f32, g as f32, b as f32, cw as f32, ww as f32]
52            }
53            Color::Rgbw { r, g, b, w } => vec![r as f32, g as f32, b as f32, w as f32],
54            Color::Rgb { r, g, b } => vec![r as f32, g as f32, b as f32],
55            Color::Hs { h, s } => vec![h, s],
56            Color::Xy { x, y } => vec![x, y],
57        }
58    }
59
60    pub fn hass_service_data_key(&self) -> &'static str {
61        match *self {
62            Color::Rgbww { .. } => "rgbww_color",
63            Color::Rgbw { .. } => "rgbw_color",
64            Color::Rgb { .. } => "rgb_color",
65            Color::Hs { .. } => "hs_color",
66            Color::Xy { .. } => "xy_color",
67        }
68    }
69
70    pub fn from_slice(mode: ColorMode, data: &[f32]) -> Option<Self> {
71        match (mode, data) {
72            (ColorMode::Rgbww, &[r, g, b, cw, ww]) => Some(Color::Rgbww {
73                r: r as u8,
74                g: g as u8,
75                b: b as u8,
76                cw: cw as u8,
77                ww: ww as u8,
78            }),
79            (ColorMode::Rgbww, _) => None,
80            (ColorMode::Rgbw, &[r, g, b, w]) => Some(Color::Rgbw {
81                r: r as u8,
82                g: g as u8,
83                b: b as u8,
84                w: w as u8,
85            }),
86            (ColorMode::Rgbw, _) => None,
87            (ColorMode::Rgb, &[r, g, b]) => Some(Color::Rgb { r: r as u8, g: g as u8, b: b as u8 }),
88            (ColorMode::Rgb, _) => None,
89            (ColorMode::Hs, &[h, s]) => Some(Color::Hs { h, s }),
90            (ColorMode::Hs, _) => None,
91            (ColorMode::Xy, &[x, y]) => Some(Color::Xy { x, y }),
92            (ColorMode::Xy, _) => None,
93            (ColorMode::ColorTemp, _) => None,
94            (ColorMode::Brightness, _) => None,
95            (ColorMode::OnOff, _) => None,
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum ColorMode {
103    Rgbww,
104    Rgbw,
105    Rgb,
106    Hs,
107    Xy,
108    ColorTemp,
109    Brightness,
110    #[serde(alias = "onoff")]
111    OnOff,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn parse_colors() {
120        assert_eq!(
121            serde_json::from_value::<Color>(
122                serde_json::json!({ "r": 255, "g": 0, "b": 128, "cw": 32, "ww": 16 })
123            )
124            .unwrap(),
125            Color::Rgbww { r: 255, g: 0, b: 128, cw: 32, ww: 16 }
126        );
127
128        assert_eq!(
129            serde_json::from_value::<Color>(
130                serde_json::json!({ "r": 255, "g": 0, "b": 128, "w": 64 })
131            )
132            .unwrap(),
133            Color::Rgbw { r: 255, g: 0, b: 128, w: 64 }
134        );
135
136        assert_eq!(
137            serde_json::from_value::<Color>(serde_json::json!({ "r": 255, "g": 0, "b": 128 }))
138                .unwrap(),
139            Color::Rgb { r: 255, g: 0, b: 128 }
140        );
141
142        assert_eq!(
143            serde_json::from_value::<Color>(serde_json::json!({ "h": 180.0, "s": 0.5 })).unwrap(),
144            Color::Hs { h: 180.0, s: 0.5 }
145        );
146
147        assert_eq!(
148            serde_json::from_value::<Color>(serde_json::json!({ "x": 0.3, "y": 0.6 })).unwrap(),
149            Color::Xy { x: 0.3, y: 0.6 }
150        );
151    }
152}