1use serde::Serialize;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum LightKind {
6 Rgb,
7 Rgbw,
8 Cct,
9 Light,
10}
11
12impl LightKind {
13 pub fn as_str(self) -> &'static str {
15 match self {
16 LightKind::Rgb => "rgb",
17 LightKind::Rgbw => "rgbw",
18 LightKind::Cct => "cct",
19 LightKind::Light => "light",
20 }
21 }
22
23 pub fn rpc_namespace(self) -> &'static str {
25 match self {
26 LightKind::Rgb => "RGB",
27 LightKind::Rgbw => "RGBW",
28 LightKind::Cct => "CCT",
29 LightKind::Light => "Light",
30 }
31 }
32
33 pub fn supports_rgb(self) -> bool {
34 matches!(self, LightKind::Rgb | LightKind::Rgbw)
35 }
36
37 pub fn supports_white(self) -> bool {
38 matches!(self, LightKind::Rgbw)
39 }
40
41 pub fn supports_ct(self) -> bool {
42 matches!(self, LightKind::Cct)
43 }
44
45 pub fn brightness_min(self) -> u8 {
47 match self {
48 LightKind::Rgb | LightKind::Rgbw => 1,
49 LightKind::Cct | LightKind::Light => 0,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct LightComponent {
57 pub kind: LightKind,
58 pub id: u8,
59}
60
61impl LightComponent {
62 pub fn from_status(status: &serde_json::Value) -> Vec<LightComponent> {
66 let kinds = [
67 ("rgb", LightKind::Rgb),
68 ("rgbw", LightKind::Rgbw),
69 ("cct", LightKind::Cct),
70 ("light", LightKind::Light),
71 ];
72 let mut out = Vec::new();
73 if let Some(obj) = status.as_object() {
74 for key in obj.keys() {
75 let Some((prefix, id_str)) = key.split_once(':') else {
76 continue;
77 };
78 if let Some((_, kind)) = kinds.iter().find(|(p, _)| *p == prefix)
79 && let Ok(id) = id_str.parse::<u8>()
80 {
81 out.push(LightComponent { kind: *kind, id });
82 }
83 }
84 }
85 out.sort_by_key(|c| (c.kind.as_str(), c.id));
86 out
87 }
88}
89
90#[derive(Debug, Clone, Default, PartialEq)]
93pub struct LightParams {
94 pub on: Option<bool>,
95 pub rgb: Option<[u8; 3]>,
96 pub white: Option<u8>,
97 pub brightness: Option<u8>,
98 pub ct: Option<u32>,
99}
100
101#[derive(Debug, Clone, Serialize, PartialEq)]
107pub struct LightStatus {
108 pub kind: String,
109 pub id: u8,
110 pub output: bool,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub brightness: Option<f64>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub rgb: Option<[u8; 3]>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub white: Option<u8>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub ct: Option<f64>,
119}
120
121impl LightStatus {
122 pub fn from_component_json(kind: LightKind, id: u8, v: &serde_json::Value) -> LightStatus {
125 let rgb = v.get("rgb").and_then(|a| a.as_array()).and_then(|a| {
126 if a.len() == 3 {
127 Some([
128 a[0].as_u64().unwrap_or(0) as u8,
129 a[1].as_u64().unwrap_or(0) as u8,
130 a[2].as_u64().unwrap_or(0) as u8,
131 ])
132 } else {
133 None
134 }
135 });
136 LightStatus {
137 kind: kind.as_str().to_string(),
138 id,
139 output: v.get("output").and_then(|o| o.as_bool()).unwrap_or(false),
140 brightness: v.get("brightness").and_then(|b| b.as_f64()),
141 rgb,
142 white: v.get("white").and_then(|w| w.as_u64()).map(|w| w as u8),
143 ct: v.get("ct").and_then(|c| c.as_f64()),
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use serde_json::json;
152
153 #[test]
154 fn capability_flags() {
155 assert!(LightKind::Rgb.supports_rgb());
156 assert!(LightKind::Rgbw.supports_rgb());
157 assert!(!LightKind::Cct.supports_rgb());
158 assert!(!LightKind::Light.supports_rgb());
159 assert!(LightKind::Rgbw.supports_white());
160 assert!(!LightKind::Rgb.supports_white());
161 assert!(LightKind::Cct.supports_ct());
162 assert_eq!(LightKind::Rgb.brightness_min(), 1);
163 assert_eq!(LightKind::Cct.brightness_min(), 0);
164 assert_eq!(LightKind::Light.brightness_min(), 0);
165 }
166
167 #[test]
168 fn detect_components_from_status() {
169 let status = json!({
170 "rgb:0": {},
171 "rgbw:0": {},
172 "cct:0": {},
173 "light:0": {},
174 "switch:0": {},
175 "sys": {},
176 });
177 let comps = LightComponent::from_status(&status);
178 assert_eq!(
179 comps,
180 vec![
181 LightComponent {
182 kind: LightKind::Cct,
183 id: 0
184 },
185 LightComponent {
186 kind: LightKind::Light,
187 id: 0
188 },
189 LightComponent {
190 kind: LightKind::Rgb,
191 id: 0
192 },
193 LightComponent {
194 kind: LightKind::Rgbw,
195 id: 0
196 },
197 ]
198 );
199 }
200
201 #[test]
202 fn detect_no_light_components() {
203 let status = json!({ "switch:0": {}, "switch:1": {}, "sys": {} });
204 assert!(LightComponent::from_status(&status).is_empty());
205 }
206
207 #[test]
208 fn parse_status_fields() {
209 let v = json!({
210 "id": 0,
211 "output": true,
212 "brightness": 80.0,
213 "rgb": [0, 255, 136]
214 });
215 let s = LightStatus::from_component_json(LightKind::Rgb, 0, &v);
216 assert!(s.output);
217 assert_eq!(s.brightness, Some(80.0));
218 assert_eq!(s.rgb, Some([0, 255, 136]));
219 assert_eq!(s.white, None);
220 }
221}