huelib2/resource/
light.rs

1#![allow(clippy::needless_update)]
2
3use crate::resource::{self, Adjust, Alert, ColorMode, Effect};
4use crate::Color;
5use derive_setters::Setters;
6use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
7
8/// A light.
9#[derive(Clone, Debug, PartialEq, Deserialize)]
10pub struct Light {
11    /// Identifier of the light.
12    #[serde(skip)]
13    pub id: String,
14    /// Name of the light.
15    pub name: String,
16    /// Type of the light.
17    #[serde(rename = "type")]
18    pub kind: String,
19    /// Current state of the light.
20    pub state: State,
21    /// The hardware model of the light.
22    #[serde(rename = "modelid")]
23    pub model_id: String,
24    /// Unique ID of the light.
25    #[serde(rename = "uniqueid")]
26    pub unique_id: String,
27    /// Product ID of the light.
28    #[serde(rename = "productid")]
29    pub product_id: Option<String>,
30    /// Product name of the light.
31    #[serde(rename = "productname")]
32    pub product_name: Option<String>,
33    /// Manufacturer name of the light.
34    #[serde(rename = "manufacturername")]
35    pub manufacturer_name: Option<String>,
36    /// The software version running on the light.
37    #[serde(rename = "swversion")]
38    pub software_version: String,
39    /// Information about software updates of the light.
40    #[cfg(not(feature = "old-api"))]
41    #[serde(rename = "swupdate")]
42    pub software_update: SoftwareUpdate,
43    /// Configuration of the light.
44    #[cfg(not(feature = "old-api"))]
45    pub config: Config,
46    /// Capabilities of the light.
47    #[cfg(not(feature = "old-api"))]
48    pub capabilities: Capabilities,
49}
50
51impl Light {
52    pub(crate) fn with_id(self, id: String) -> Self {
53        Self { id, ..self }
54    }
55}
56
57impl resource::Resource for Light {}
58
59/// State of a light.
60#[derive(Clone, Debug, PartialEq, Deserialize)]
61pub struct State {
62    /// Whether the light is on.
63    pub on: Option<bool>,
64    /// Brightness of the light.
65    ///
66    /// The maximum brightness is 254 and 1 is the minimum brightness.
67    #[serde(rename = "bri")]
68    pub brightness: Option<u8>,
69    /// Hue of the light.
70    ///
71    /// Both 0 and 65535 are red, 25500 is green and 46920 is blue.
72    pub hue: Option<u16>,
73    /// Saturation of the light.
74    ///
75    /// The most saturated (colored) is 254 and 0 is the least saturated (white).
76    #[serde(rename = "sat")]
77    pub saturation: Option<u8>,
78    /// X and y coordinates of a color in CIE color space. Both values must be between 0 and 1.
79    #[serde(rename = "xy")]
80    pub color_space_coordinates: Option<(f32, f32)>,
81    /// Mired color temperature of the light.
82    #[serde(rename = "ct")]
83    pub color_temperature: Option<u16>,
84    /// Alert effect of the light.
85    pub alert: Option<Alert>,
86    /// Dynamic effect of the light.
87    pub effect: Option<Effect>,
88    /// Color mode of the light.
89    #[serde(rename = "colormode")]
90    pub color_mode: Option<ColorMode>,
91    /// Whether the light can be reached by the bridge.
92    pub reachable: bool,
93}
94
95/// Information about software updates of a light.
96#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
97pub struct SoftwareUpdate {
98    /// State of software updates.
99    pub state: SoftwareUpdateState,
100    /// When the last update was installed.
101    #[serde(rename = "lastinstall")]
102    pub last_install: Option<chrono::NaiveDateTime>,
103}
104
105/// State of a software update.
106///
107/// See [this issue] for the reason why this enum is marked as `non_exhaustive`.
108///
109/// [this issue]: https://github.com/yuqio/huelib-rs/issues/1
110#[non_exhaustive]
111#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum SoftwareUpdateState {
114    /// Error
115    Error,
116    /// Installing
117    Installing,
118    /// No updates are available.
119    NoUpdates,
120    /// Device cannot be updated.
121    NotUpdatable,
122    /// Device is downloading new updates.
123    Transferring,
124    /// Device is ready to install new updates.
125    ReadyToInstall,
126    // FIXME: Add missing variants for states (https://github.com/yuqio/huelib-rs/issues/1)
127}
128
129/// Configuration of a light.
130#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
131pub struct Config {
132    /// Arche type of the light.
133    #[serde(rename = "archetype")]
134    pub arche_type: String,
135    /// Function of the light.
136    pub function: String,
137    /// Direction of the light.
138    pub direction: String,
139    /// Startup configuration of the light.
140    pub startup: Option<StartupConfig>,
141}
142
143/// Startup configuration of a light.
144#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
145pub struct StartupConfig {
146    /// Mode of the startup.
147    pub mode: String,
148    /// Whether startup is configured for the light.
149    pub configured: bool,
150}
151
152/// Capabilities of a light.
153#[derive(Clone, Debug, PartialEq, Deserialize)]
154pub struct Capabilities {
155    /// Whether the light is certified.
156    pub certified: bool,
157    /// Control capabilities of the light.
158    pub control: ControlCapabilities,
159    /// Streaming capabilities of the light.
160    pub streaming: StreamingCapabilities,
161}
162
163/// Control capabilities of a light.
164#[derive(Clone, Debug, PartialEq, Deserialize)]
165pub struct ControlCapabilities {
166    /// Minimal dimlevel of the light.
167    #[serde(rename = "mindimlevel")]
168    pub min_dimlevel: Option<usize>,
169    /// Maximal lumen of the light.
170    #[serde(rename = "maxlumen")]
171    pub max_lumen: Option<usize>,
172    /// Color gamut of the light.
173    #[serde(rename = "colorgamut")]
174    pub color_gamut: Option<Vec<(f32, f32)>>,
175    /// Type of the color gamut of the light.
176    #[serde(rename = "colorgamuttype")]
177    pub color_gamut_type: Option<String>,
178    /// Maximal/minimal color temperature of the light.
179    #[serde(rename = "ct")]
180    pub color_temperature: Option<ColorTemperatureCapabilities>,
181}
182
183/// Color temperature capabilities of a light.
184#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
185pub struct ColorTemperatureCapabilities {
186    /// Minimal color temperature.
187    pub min: usize,
188    /// Maximal color temperature.
189    pub max: usize,
190}
191
192/// Streaming capabilities of a light.
193#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
194pub struct StreamingCapabilities {
195    /// Whether a renderer is enabled.
196    pub renderer: bool,
197    /// Whether a proxy is enabled.
198    pub proxy: bool,
199}
200
201/// Modifier for light attributes.
202#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Setters)]
203#[setters(strip_option, prefix = "with_")]
204pub struct AttributeModifier {
205    /// Sets the name of the light.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub name: Option<String>,
208}
209
210impl AttributeModifier {
211    /// Creates a new [`AttributeModifier`].
212    pub fn new() -> Self {
213        Self::default()
214    }
215}
216
217impl resource::Modifier for AttributeModifier {
218    type Id = String;
219    fn url_suffix(id: Self::Id) -> String {
220        format!("lights/{}", id)
221    }
222}
223
224/// Static modifier for the light state.
225///
226/// In comparison to [`StateModifier`], this modifier cannot increment/decrement any attributes or
227/// change the alert effect.
228///
229/// This modifier is used in [`scene::Modifier`] and [`scene::Creator`].
230///
231/// [`scene::Modifier`]: super::scene::Modifier
232/// [`scene::Creator`]: super::scene::Creator
233#[derive(Clone, Debug, Default, PartialEq, Serialize, Setters)]
234#[setters(strip_option, prefix = "with_")]
235pub struct StaticStateModifier {
236    /// Turns the light on or off.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub on: Option<bool>,
239    /// Sets the brightness of the light.
240    #[serde(skip_serializing_if = "Option::is_none", rename = "bri")]
241    pub brightness: Option<u8>,
242    /// Sets the hue of the light.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub hue: Option<u16>,
245    /// Sets the saturation of a light.
246    #[serde(skip_serializing_if = "Option::is_none", rename = "sat")]
247    pub saturation: Option<u8>,
248    /// Sets the color space coordinates of the light.
249    #[serde(skip_serializing_if = "Option::is_none", rename = "xy")]
250    pub color_space_coordinates: Option<(f32, f32)>,
251    /// Sets the color temperature of a light.
252    #[serde(skip_serializing_if = "Option::is_none", rename = "ct")]
253    pub color_temperature: Option<u16>,
254    /// Sets the dynamic effect of a light.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub effect: Option<Effect>,
257    /// Sets the transition duration of state changes.
258    ///
259    /// This is given as a multiple of 100ms.
260    #[serde(skip_serializing_if = "Option::is_none", rename = "transitiontime")]
261    pub transition_time: Option<u16>,
262}
263
264impl StaticStateModifier {
265    /// Creates a new [`StaticStateModifier`].
266    pub fn new() -> Self {
267        Self::default()
268    }
269
270    /// Convenient method to set the [`color_space_coordinates`] and [`brightness`] fields.
271    ///
272    /// [`color_space_coordinates`]: Self::color_space_coordinates
273    /// [`brightness`]: Self::brightness
274    pub fn with_color(self, value: Color) -> Self {
275        let mut modifier = Self {
276            color_space_coordinates: Some(value.space_coordinates),
277            ..self
278        };
279        if let Some(brightness) = value.brightness {
280            modifier.brightness = Some(brightness);
281        }
282        modifier
283    }
284}
285
286impl resource::Modifier for StaticStateModifier {
287    type Id = String;
288    fn url_suffix(id: Self::Id) -> String {
289        format!("lights/{}/state", id)
290    }
291}
292
293/// Modifier for the light state.
294#[derive(Clone, Debug, Default, PartialEq, Setters)]
295#[setters(strip_option, prefix = "with_")]
296pub struct StateModifier {
297    /// Turns the light on or off.
298    pub on: Option<bool>,
299    /// Sets the brightness of the light.
300    pub brightness: Option<Adjust<u8>>,
301    /// Sets the hue of a light.
302    pub hue: Option<Adjust<u16>>,
303    /// Sets the saturation of a light.
304    pub saturation: Option<Adjust<u8>>,
305    /// Sets the color space coordinates of the light.
306    pub color_space_coordinates: Option<Adjust<(f32, f32)>>,
307    /// Sets the color temperature of a light.
308    pub color_temperature: Option<Adjust<u16>>,
309    /// Sets the alert effect of a light.
310    pub alert: Option<Alert>,
311    /// Sets the dynamic effect of a light.
312    pub effect: Option<Effect>,
313    /// Sets the transition duration of state changes.
314    ///
315    /// This is given as a multiple of 100ms.
316    pub transition_time: Option<u16>,
317}
318
319impl StateModifier {
320    /// Creates a new [`StateModifier`].
321    pub fn new() -> Self {
322        Self::default()
323    }
324
325    /// Convenient method to set the [`color_space_coordinates`] and [`brightness`] fields.
326    ///
327    /// [`color_space_coordinates`]: Self::color_space_coordinates
328    /// [`brightness`]: Self::brightness
329    pub fn with_color(self, value: Color) -> Self {
330        let mut modifier = Self {
331            color_space_coordinates: Some(Adjust::Override(value.space_coordinates)),
332            ..self
333        };
334        if let Some(brightness) = value.brightness {
335            modifier.brightness = Some(Adjust::Override(brightness));
336        }
337        modifier
338    }
339}
340
341impl resource::Modifier for StateModifier {
342    type Id = String;
343    fn url_suffix(id: Self::Id) -> String {
344        format!("lights/{}/state", id)
345    }
346}
347
348impl Serialize for StateModifier {
349    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
350    where
351        S: Serializer,
352    {
353        custom_serialize! {
354            serializer, "StateModifier";
355            on => (&self.on),
356            bri => (&self.brightness, to_override),
357            bri_inc => (&self.brightness, to_increment, i16),
358            hue => (&self.hue, to_override),
359            hue_inc => (&self.hue, to_increment, i32),
360            sat => (&self.saturation, to_override),
361            sat_inc => (&self.saturation, to_increment, i16),
362            xy => (&self.color_space_coordinates, to_override),
363            xy_inc => (&self.color_space_coordinates, to_increment_tuple, f32),
364            ct => (&self.color_temperature, to_override),
365            ct_inc => (&self.color_temperature, to_increment, i32),
366            alert => (&self.alert),
367            effect => (&self.effect),
368            transitiontime => (&self.transition_time),
369        }
370    }
371}
372
373/// Scanner for new lights.
374#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Setters)]
375#[setters(strip_option, prefix = "with_")]
376pub struct Scanner {
377    /// The device identifiers.
378    #[serde(skip_serializing_if = "Option::is_none", rename = "deviceid")]
379    pub device_ids: Option<Vec<String>>,
380}
381
382impl Scanner {
383    /// Creates a new [`Scanner`].
384    pub fn new() -> Self {
385        Self::default()
386    }
387}
388
389impl resource::Scanner for Scanner {
390    fn url_suffix() -> String {
391        "lights".to_owned()
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use serde_json::json;
399
400    #[test]
401    fn serialize_attribute_modifier() {
402        let modifier = AttributeModifier::new();
403        let modifier_json = serde_json::to_value(modifier).unwrap();
404        let expected_json = json!({});
405        assert_eq!(modifier_json, expected_json);
406
407        let modifier = AttributeModifier {
408            name: Some("test".into()),
409        };
410        let modifier_json = serde_json::to_value(modifier).unwrap();
411        let expected_json = json!({"name": "test"});
412        assert_eq!(modifier_json, expected_json);
413    }
414
415    #[test]
416    fn serialize_static_state_modifier() {
417        let modifier = StaticStateModifier::new();
418        let modifier_json = serde_json::to_value(modifier).unwrap();
419        let expected_json = json!({});
420        assert_eq!(modifier_json, expected_json);
421
422        let modifier = StaticStateModifier {
423            on: Some(true),
424            brightness: Some(1),
425            hue: Some(2),
426            saturation: Some(3),
427            color_space_coordinates: None,
428            color_temperature: Some(4),
429            effect: Some(Effect::Colorloop),
430            transition_time: Some(4),
431        };
432        let modifier_json = serde_json::to_value(modifier).unwrap();
433        let expected_json = json!({
434            "on": true,
435            "bri": 1,
436            "hue": 2,
437            "sat": 3,
438            "ct": 4,
439            "effect": "colorloop",
440            "transitiontime": 4,
441        });
442        assert_eq!(modifier_json, expected_json);
443
444        let modifier = StaticStateModifier::new()
445            .with_brightness(1)
446            .with_color(Color::from_rgb(0, 0, 0));
447        let modifier_json = serde_json::to_value(modifier).unwrap();
448        let expected_json = json!({
449            "bri": 0,
450            "xy": [0.0, 0.0]
451        });
452        assert_eq!(modifier_json, expected_json);
453    }
454
455    #[test]
456    fn serialize_state_modifier() {
457        let modifier = StateModifier::new();
458        let modifier_json = serde_json::to_value(modifier).unwrap();
459        let expected_json = json!({});
460        assert_eq!(modifier_json, expected_json);
461
462        let modifier = StateModifier {
463            on: Some(true),
464            brightness: Some(Adjust::Increment(1)),
465            hue: Some(Adjust::Override(2)),
466            saturation: Some(Adjust::Decrement(3)),
467            color_space_coordinates: None,
468            color_temperature: Some(Adjust::Override(4)),
469            alert: Some(Alert::None),
470            effect: Some(Effect::Colorloop),
471            transition_time: Some(4),
472        };
473        let modifier_json = serde_json::to_value(modifier).unwrap();
474        let expected_json = json!({
475            "on": true,
476            "bri_inc": 1,
477            "hue": 2,
478            "sat_inc": -3,
479            "ct": 4,
480            "alert": "none",
481            "effect": "colorloop",
482            "transitiontime": 4,
483        });
484        assert_eq!(modifier_json, expected_json);
485
486        let modifier = StateModifier::new()
487            .with_brightness(Adjust::Increment(1))
488            .with_color(Color::from_rgb(0, 0, 0));
489        let modifier_json = serde_json::to_value(modifier).unwrap();
490        let expected_json = json!({
491            "bri": 0,
492            "xy": [0.0, 0.0]
493        });
494        assert_eq!(modifier_json, expected_json);
495    }
496
497    #[test]
498    fn serialize_scanner() {
499        let scanner = Scanner::new();
500        let scanner_json = serde_json::to_value(scanner).unwrap();
501        let expected_json = json!({});
502        assert_eq!(scanner_json, expected_json);
503
504        let scanner = Scanner {
505            device_ids: Some(vec!["1".into()]),
506        };
507        let scanner_json = serde_json::to_value(scanner).unwrap();
508        let expected_json = json!({
509            "deviceid": ["1"]
510        });
511        assert_eq!(scanner_json, expected_json);
512    }
513}