riz/
models.rs

1//! Riz models
2
3use std::collections::HashMap;
4use std::net::{Ipv4Addr, UdpSocket};
5use std::result::Result as StdResult;
6use std::str::FromStr;
7use std::time::Duration;
8
9use log::debug;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use strum::IntoEnumIterator;
13use strum_macros::EnumIter;
14use utoipa::ToSchema;
15use uuid::Uuid;
16
17use crate::{Error, Result};
18
19/// Rooms group lights logically to allow for batched actions
20///
21/// NB: They don't have to be the same as configured by the Wiz app
22///
23#[serde_with::skip_serializing_none]
24#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
25pub struct Room {
26    #[schema(min_length = 1, max_length = 100)]
27    name: String,
28    #[schema(max_items = 100)]
29    lights: Option<HashMap<Uuid, Light>>,
30
31    #[serde(skip)]
32    id: Uuid,
33    #[serde(skip)]
34    linked: bool,
35}
36
37impl Room {
38    /// Create a new room with some name and no lights
39    pub fn new(name: &str) -> Self {
40        Room {
41            name: String::from(name),
42            lights: None,
43            id: Uuid::new_v4(),
44            linked: false,
45        }
46    }
47
48    /// Link the id to this Room for self-reference
49    ///
50    /// Can only be called once
51    ///
52    /// # Panics
53    ///   If called more than once
54    ///
55    pub fn link(&mut self, id: &Uuid) {
56        if self.linked {
57            panic!("refusing to overwrite id!")
58        }
59        self.id = *id;
60        self.linked = true;
61    }
62
63    /// Ask all bulbs in this room for their current status
64    ///
65    /// # Returns
66    ///   a [Result] of:
67    ///   (unordered) [Vec] of [LightingResponse] from all bulbs on success
68    ///   and [Error] if there's any error getting status from any bulb
69    ///
70    pub fn get_status(&mut self) -> Result<Vec<LightingResponse>> {
71        let mut resp = Vec::new();
72        if let Some(lights) = &mut self.lights {
73            for light in lights.values_mut() {
74                let status = light.get_status()?;
75                resp.push(LightingResponse::status(light.ip, status));
76            }
77        }
78        Ok(resp)
79    }
80
81    /// Store a newly created [Light] in this room
82    ///
83    /// Will generate a new [Uuid] and store the [Light] in this lights.
84    ///
85    /// # Returns
86    ///   the newly created [Uuid] for the [Light]
87    ///
88    pub fn new_light(&mut self, light: Light) -> Result<Uuid> {
89        self.validate_light(&light, None)?;
90        let mut id = Uuid::new_v4();
91        if let Some(lights) = self.lights.as_mut() {
92            while lights.contains_key(&id) {
93                id = Uuid::new_v4();
94            }
95            lights.insert(id, light);
96        } else {
97            self.lights = Some(HashMap::from([(id, light)]));
98        }
99        Ok(id)
100    }
101
102    /// Removes a light from the room's lights
103    ///
104    /// # Returns
105    ///   [Err] [String] when unable to find the light ID or no lights
106    ///
107    pub fn delete_light(&mut self, light: &Uuid) -> Result<()> {
108        if let Some(lights) = self.lights.as_mut() {
109            match lights.remove(light) {
110                Some(_) => Ok(()),
111                None => Err(Error::light_not_found(&self.id, light)),
112            }
113        } else {
114            Err(Error::RoomNotFound(self.id))
115        }
116    }
117
118    /// Update the non-lighting settings of a light bulb
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use std::str::FromStr;
124    /// use std::net::Ipv4Addr;
125    /// use riz::models::{Room, Light};
126    ///
127    /// let ip1 = Ipv4Addr::from_str("10.1.2.3").unwrap();
128    /// let ip2 = Ipv4Addr::from_str("10.1.2.4").unwrap();
129    ///
130    /// let mut room = Room::new("test");
131    ///
132    /// let light = Light::new(ip1, Some("foo"));
133    /// let light_id = room.new_light(light).unwrap();
134    ///
135    /// let read = room.read(&light_id).unwrap();
136    /// assert_eq!(read.name(), Some("foo"));
137    /// assert_eq!(read.ip(), ip1);
138    ///
139    /// room.update_light(&light_id, &Light::new(ip2, Some("bar"))).unwrap();
140    ///
141    /// let read = room.read(&light_id).unwrap();
142    /// assert_eq!(read.name(), Some("bar"));
143    /// assert_eq!(read.ip(), ip2);
144    /// ```
145    ///
146    /// # Returns
147    ///   [Err] [String] if either room or light id is not known
148    ///
149    pub fn update_light(&mut self, id: &Uuid, light: &Light) -> Result<()> {
150        if let Some(lights) = self.lights.as_mut() {
151            match lights.get_mut(id) {
152                Some(l) => {
153                    if l.update(light) {
154                        Ok(())
155                    } else {
156                        Err(Error::no_change_light(&self.id, id))
157                    }
158                }
159                None => Err(Error::light_not_found(&self.id, id)),
160            }
161        } else {
162            Err(Error::NoLights(self.id))
163        }
164    }
165
166    /// List all lights in this room, if any
167    ///
168    /// # Returns
169    ///   [Vec] of &[Uuid]; valid [Light] IDs
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use std::str::FromStr;
175    /// use std::net::Ipv4Addr;
176    /// use riz::models::{Room, Light};
177    ///
178    /// let mut room = Room::new("test");
179    /// assert!(room.list().is_none());
180    ///
181    /// let light = Light::new(Ipv4Addr::from_str("10.1.2.3").unwrap(), None);
182    /// let light_id = room.new_light(light).unwrap();
183    ///
184    /// let ids = room.list().unwrap();
185    /// assert_eq!(*ids.iter().next().unwrap(), &light_id);
186    /// ```
187    ///
188    pub fn list(&self) -> Option<Vec<&Uuid>> {
189        self.lights.as_ref().map(|lights| lights.keys().collect())
190    }
191
192    /// Read a light in this room by ID
193    ///
194    /// # Returns
195    ///   [Option] of &[Light] if the &[Uuid] if known
196    ///
197    pub fn read(&self, light: &Uuid) -> Option<&Light> {
198        match &self.lights {
199            Some(lights) => lights.get(light),
200            None => None,
201        }
202    }
203
204    /// Read a light in this room by ID as a mutable reference
205    ///
206    /// # Returns
207    ///   [Option] of &mut [Light] if the &[Uuid] is known
208    ///
209    pub fn read_mut(&mut self, light: &Uuid) -> Option<&mut Light> {
210        if let Some(lights) = self.lights.as_mut() {
211            lights.get_mut(light)
212        } else {
213            None
214        }
215    }
216
217    /// Process a reply from a lighting request for bulbs in this room
218    ///
219    /// # Returns
220    ///   [bool] of if any of this room's lights were updated
221    ///
222    pub fn process_reply(&mut self, resp: &LightingResponse) -> bool {
223        let mut any_update = false;
224        if let Some(lights) = self.lights.as_mut() {
225            for light in lights.values_mut() {
226                let light_update = light.process_reply(resp);
227                any_update = any_update || light_update;
228            }
229        }
230        any_update
231    }
232
233    /// Accessor for this room's name
234    pub fn name(&self) -> &str {
235        &self.name
236    }
237
238    /// Update our (non-light) attributes from the other instance
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use riz::models::Room;
244    ///
245    /// let mut room = Room::new("foo");
246    /// let other = Room::new("bar");
247    /// assert!(room.update(&other));
248    /// assert_eq!(room.name(), "bar");
249    /// ```
250    ///
251    pub fn update(&mut self, other: &Self) -> bool {
252        if self.name == other.name {
253            return false;
254        }
255        self.name = other.name.clone();
256        true
257    }
258
259    fn validate_light(&self, light: &Light, light_id: Option<&Uuid>) -> Result<()> {
260        let ip = light.ip();
261        if let Some(lights) = self.lights.as_ref() {
262            for (id, known) in lights {
263                if Some(id) == light_id {
264                    continue;
265                }
266                if known.ip() == ip {
267                    return Err(Error::invalid_ip(&ip, "already known"));
268                }
269            }
270        }
271        Ok(())
272    }
273}
274
275/// Lights are grouped per room, or used individually by the CLI
276///
277/// # Examples
278///
279/// ```
280/// use std::net::Ipv4Addr;
281/// use std::str::FromStr;
282/// use riz::models::Light;
283///
284/// let light = Light::new(Ipv4Addr::from_str("10.1.2.3").unwrap(), None);
285/// assert!(light.status().is_none());
286/// ```
287///
288#[serde_with::skip_serializing_none]
289#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
290pub struct Light {
291    /// IPv4 address for the light, ideally statically assigned
292    #[schema(
293      min_length = 1,
294      max_length = 15,
295      value_type = String,
296      example = "192.168.1.50",
297      pattern = r"^(((1[\d]{0,2})|(2([0-4]?[\d]|5[0-5]))|([3-9]?[\d])|[\d])\.){0,3}((1[\d]{0,2})|(2([0-4]?[\d]|5[0-5]))|([3-9]?[\d])|[\d])$")]
298    ip: Ipv4Addr,
299
300    /// Name of light, arbitrary (user supplied)
301    #[schema(min_length = 1, max_length = 100)]
302    name: Option<String>,
303
304    /// Last known status, if any
305    status: Option<LightStatus>,
306}
307
308impl Light {
309    /// Create a new optionally named light with no known status
310    pub fn new(ip: Ipv4Addr, name: Option<&str>) -> Self {
311        Light {
312            ip,
313            name: name.map(String::from),
314            status: None,
315        }
316    }
317
318    /// Accessor for this bulb's IP address
319    pub fn ip(&self) -> Ipv4Addr {
320        self.ip
321    }
322
323    /// Accessor for this bulb's name
324    pub fn name(&self) -> Option<&str> {
325        match &self.name {
326            Some(s) => Some(s),
327            None => None,
328        }
329    }
330
331    /// Accessor for this bulb's last known status
332    pub fn status(&self) -> Option<&LightStatus> {
333        self.status.as_ref()
334    }
335
336    /// Ask the bulb for its status
337    ///
338    /// Note that this is not the same as accessing the last known
339    /// status for the bulb, this method sends a new request for data,
340    ///
341    /// If you want to update the last known state, you can pass the
342    /// newly fetched status into [Self::process_reply]
343    ///
344    pub fn get_status(&self) -> Result<LightStatus> {
345        let resp = self.udp_response(&json!({"method": "getPilot"}))?;
346
347        let status: BulbStatus = match serde_json::from_value(resp) {
348            Ok(v) => v,
349            Err(e) => return Err(Error::JsonLoad(e)),
350        };
351        let status = LightStatus::from(&status);
352        Ok(status)
353    }
354
355    /// Set new lighting settings on this bulb
356    ///
357    /// Does not update self.status, you can pass the response back
358    /// into [Self::process_reply] if you want to update the internal state
359    ///
360    pub fn set(&self, payload: &Payload) -> Result<LightingResponse> {
361        if payload.is_valid() {
362            match serde_json::to_value(payload) {
363                Ok(msg) => match self.udp_response(&json!({
364                  "method": "setPilot",
365                  "params": msg,
366                })) {
367                    Ok(v) => {
368                        debug!("udp response: {:?}", v);
369                        Ok(LightingResponse::payload(self.ip, payload.clone()))
370                    }
371                    Err(e) => Err(e),
372                },
373                Err(e) => Err(Error::JsonDump(e)),
374            }
375        } else {
376            Err(Error::NoAttribute)
377        }
378    }
379
380    /// Set the [PowerMode] for the light
381    ///
382    /// Works in the same fashion as [Self::set], where the action does not
383    /// mutate internal state. You can pass the response from this method
384    /// to [Self::process_reply] if you want to update this bulb's status
385    ///
386    pub fn set_power(&self, power: &PowerMode) -> Result<LightingResponse> {
387        match power {
388            PowerMode::On => self.toggle_power(true),
389            PowerMode::Off => self.toggle_power(false),
390            PowerMode::Reboot => self.power_cycle(),
391        }
392    }
393
394    fn toggle_power(&self, powered: bool) -> Result<LightingResponse> {
395        self.udp_response(&json!({"method": "setState","params": { "state": powered }}))?;
396        Ok(if powered {
397            LightingResponse::power(self.ip, PowerMode::On)
398        } else {
399            LightingResponse::power(self.ip, PowerMode::Off)
400        })
401    }
402
403    fn power_cycle(&self) -> Result<LightingResponse> {
404        self.udp_response(&json!({"method": "reboot"}))?;
405        Ok(LightingResponse::power(self.ip, PowerMode::Reboot))
406    }
407
408    /// Update this light's non-lighting attributes
409    fn update(&mut self, other: &Self) -> bool {
410        let mut any_update = false;
411        if self.name != other.name {
412            self.name = other.name.clone();
413            any_update = true;
414        }
415
416        if self.ip != other.ip {
417            self.ip = other.ip;
418            any_update = true;
419        }
420
421        any_update
422    }
423
424    /// Update the internal state with the response of some command
425    pub fn process_reply(&mut self, resp: &LightingResponse) -> bool {
426        if resp.ip == self.ip {
427            match &resp.response {
428                LightingResponseType::Payload(payload) => self.update_status_from_payload(payload),
429                LightingResponseType::Power(power) => self.update_status_from_power(power),
430                LightingResponseType::Status(status) => self.update_status(status),
431            }
432            true
433        } else {
434            false
435        }
436    }
437
438    fn update_status(&mut self, status: &LightStatus) {
439        if let Some(known) = &mut self.status {
440            known.update(status);
441        } else {
442            self.status = Some(status.clone());
443        }
444    }
445
446    fn update_status_from_payload(&mut self, payload: &Payload) {
447        if let Some(status) = &mut self.status {
448            status.update_from_payload(payload);
449        } else {
450            self.status = Some(LightStatus::from(payload));
451        }
452    }
453
454    fn update_status_from_power(&mut self, power: &PowerMode) {
455        if let Some(status) = &mut self.status {
456            status.update_from_power(power);
457        } else {
458            self.status = Some(LightStatus::from(power));
459        }
460    }
461
462    fn udp_response(&self, msg: &Value) -> Result<Value> {
463        // dump the control message to string
464        let msg = match serde_json::to_string(&msg) {
465            Ok(v) => v,
466            Err(e) => return Err(Error::JsonDump(e)),
467        };
468
469        // get some udp socket from the os
470        let socket = match UdpSocket::bind("0.0.0.0:0") {
471            Ok(s) => s,
472            Err(e) => return Err(Error::socket("bind", e)),
473        };
474
475        // set a 1 second read and write timeout
476        match socket.set_write_timeout(Some(Duration::new(1, 0))) {
477            Ok(_) => {}
478            Err(e) => return Err(Error::socket("set_write_timeout", e)),
479        };
480
481        match socket.set_read_timeout(Some(Duration::new(1, 0))) {
482            Ok(_) => {}
483            Err(e) => return Err(Error::socket("set_read_timeout", e)),
484        };
485
486        // connect to the remote bulb at their standard port
487        match socket.connect(format!("{}:38899", self.ip)) {
488            Ok(_) => {}
489            Err(e) => return Err(Error::socket("connect", e)),
490        }
491
492        // send the control message
493        match socket.send(msg.as_bytes()) {
494            Ok(_) => {}
495            Err(e) => return Err(Error::socket("send", e)),
496        };
497
498        // declare a buffer of the max message size
499        let mut buffer = [0; 4096];
500        let bytes = match socket.recv(&mut buffer) {
501            Ok(b) => b,
502            Err(e) => return Err(Error::socket("receive", e)),
503        };
504
505        // Redeclare `buffer` as String of the received bytes
506        let buffer = match String::from_utf8(buffer[..bytes].to_vec()) {
507            Ok(s) => s,
508            Err(e) => return Err(Error::Utf8Decode(e)),
509        };
510
511        // create some JSON object from the string
512        match serde_json::from_str(&buffer) {
513            Ok(v) => Ok(v),
514            Err(e) => Err(Error::JsonLoad(e)),
515        }
516    }
517}
518
519/// Brightness can be applied in any context, values from 10 to 100
520#[derive(Default, Debug, Serialize, Deserialize, Clone, ToSchema)]
521pub struct Brightness {
522    #[schema(minimum = 10, maximum = 100)]
523    value: u8,
524}
525
526impl Brightness {
527    /// Create a new Brightness with the default value
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use riz::models::Brightness;
533    ///
534    /// let brightness = Brightness::new();
535    /// assert_eq!(brightness.value(), 100);
536    /// ```
537    pub fn new() -> Self {
538        Brightness { value: 100 }
539    }
540
541    /// Accessor for our read-only value
542    pub fn value(&self) -> u8 {
543        self.value
544    }
545
546    /// Create a new Brightness value with the given value
547    ///
548    /// # Returns
549    ///   [Option] of [Brightness] when value is within the valid range
550    ///
551    /// # Examples
552    ///
553    /// ```
554    /// use riz::models::Brightness;
555    ///
556    /// assert!(Brightness::create(9).is_none());
557    /// assert!(Brightness::create(10).is_some());
558    /// assert!(Brightness::create(100).is_some());
559    /// assert!(Brightness::create(101).is_none());
560    /// ```
561    ///
562    pub fn create(value: u8) -> Option<Self> {
563        if Self::valid(value) {
564            Some(Brightness { value })
565        } else {
566            None
567        }
568    }
569
570    /// Create a new Brightness value with the given value or the
571    /// default if the value is not within the valid range
572    ///
573    /// # Examples
574    ///
575    /// ```
576    /// use riz::models::Brightness;
577    ///
578    /// assert_eq!(Brightness::create_or(9).value(), 100);
579    /// assert_eq!(Brightness::create_or(10).value(), 10);
580    /// assert_eq!(Brightness::create_or(100).value(), 100);
581    /// assert_eq!(Brightness::create_or(101).value(), 100);
582    /// ```
583    ///
584    pub fn create_or(value: u8) -> Self {
585        Brightness {
586            value: if Self::valid(value) { value } else { 100 },
587        }
588    }
589
590    /// Check if the value is within the valid range
591    fn valid(value: u8) -> bool {
592        (10..=100).contains(&value)
593    }
594}
595
596/// Speed can be applied to select scenes only, values from 20 to 200
597#[derive(Default, Debug, Serialize, Deserialize, Clone, ToSchema)]
598pub struct Speed {
599    #[schema(minimum = 20, maximum = 200)]
600    value: u8,
601}
602
603impl Speed {
604    /// Create a new speed setting with the default value
605    ///
606    /// # Examples
607    ///
608    /// ```
609    /// use riz::models::Speed;
610    ///
611    /// assert_eq!(Speed::new().value(), 100);
612    /// ```
613    ///
614    pub fn new() -> Self {
615        Speed { value: 100 }
616    }
617
618    /// Accessor for our read-only value
619    pub fn value(&self) -> u8 {
620        self.value
621    }
622
623    /// Create a new speed setting with the given value
624    ///
625    /// # Returns
626    ///   [Speed] when value is within the valid range
627    ///
628    /// # Examples
629    ///
630    /// ```
631    /// use riz::models::Speed;
632    ///
633    /// assert!(Speed::create(19).is_none());
634    /// assert!(Speed::create(20).is_some());
635    /// assert!(Speed::create(200).is_some());
636    /// assert!(Speed::create(201).is_none());
637    /// ```
638    ///
639    pub fn create(value: u8) -> Option<Self> {
640        if Self::valid(value) {
641            Some(Speed { value })
642        } else {
643            None
644        }
645    }
646
647    /// Create a new speed setting with the given value if within
648    /// the valid range, otherwise the default value
649    ///
650    /// # Examples
651    ///
652    /// ```
653    /// use riz::models::Speed;
654    ///
655    /// assert_eq!(Speed::create_or(19).value(), 100);
656    /// assert_eq!(Speed::create_or(20).value(), 20);
657    /// assert_eq!(Speed::create_or(200).value(), 200);
658    /// assert_eq!(Speed::create_or(201).value(), 100);
659    /// ```
660    ///
661    pub fn create_or(value: u8) -> Self {
662        Speed {
663            value: if Self::valid(value) { value } else { 100 },
664        }
665    }
666
667    fn valid(value: u8) -> bool {
668        (20..=200).contains(&value)
669    }
670}
671
672/// Kelvin sets a temperature mode, values from 1000 to 8000
673#[derive(Default, Debug, Serialize, Deserialize, Clone, ToSchema)]
674pub struct Kelvin {
675    #[schema(minimum = 1000, maximum = 8000)]
676    kelvin: u16,
677}
678
679impl Kelvin {
680    /// Create a new Kelvin setting with the default value
681    ///
682    /// # Examples
683    ///
684    /// ```
685    /// use riz::models::Kelvin;
686    ///
687    /// assert_eq!(Kelvin::new().kelvin(), 1000);
688    /// ```
689    ///
690    pub fn new() -> Self {
691        Kelvin { kelvin: 1000 }
692    }
693
694    /// Accessor for our read-only kelvin setting
695    pub fn kelvin(&self) -> u16 {
696        self.kelvin
697    }
698
699    /// Create a new Kelvin setting with the given value
700    ///
701    /// # Returns
702    ///   [Kelvin] when value is within the valid range
703    ///
704    /// # Examples
705    ///
706    /// ```
707    /// use riz::models::Kelvin;
708    ///
709    /// assert!(Kelvin::create(999).is_none());
710    /// assert!(Kelvin::create(1000).is_some());
711    /// assert!(Kelvin::create(8000).is_some());
712    /// assert!(Kelvin::create(8001).is_none());
713    /// ```
714    ///
715    pub fn create(kelvin: u16) -> Option<Self> {
716        if (1000..=8000).contains(&kelvin) {
717            Some(Kelvin { kelvin })
718        } else {
719            None
720        }
721    }
722}
723
724/// White describes a cool or warm white mode, values from 1 to 100
725#[derive(Default, Debug, Serialize, Deserialize, Clone, ToSchema)]
726pub struct White {
727    #[schema(minimum = 1, maximum = 100)]
728    value: u8,
729}
730
731impl White {
732    /// Create a new white setting with the default value
733    pub fn new() -> Self {
734        White { value: 100 }
735    }
736
737    /// Create a new white setting with the given value
738    ///
739    /// # Returns
740    ///   [White] if the value provided is within the valid range
741    ///
742    /// # Examples
743    ///
744    /// ```
745    /// use riz::models::White;
746    ///
747    /// assert!(White::create(0).is_none());
748    /// assert!(White::create(1).is_some());
749    /// assert!(White::create(100).is_some());
750    /// assert!(White::create(101).is_none());
751    /// ```
752    ///
753    pub fn create(value: u8) -> Option<Self> {
754        if (1..=100).contains(&value) {
755            Some(White { value })
756        } else {
757            None
758        }
759    }
760}
761
762/// Color is any RGB color, values from 0 to 255
763#[derive(Default, Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq)]
764pub struct Color {
765    #[schema(maximum = 255)]
766    red: u8,
767    #[schema(maximum = 255)]
768    green: u8,
769    #[schema(maximum = 255)]
770    blue: u8,
771}
772
773impl Color {
774    /// Create a new default color
775    ///
776    /// # Examples
777    ///
778    /// ```
779    /// use std::str::FromStr;
780    /// use riz::models::Color;
781    ///
782    /// assert_eq!(Color::new(), Color::from_str("0,0,0").unwrap());
783    /// assert_ne!(Color::new(), Color::from_str("0,1,0").unwrap());
784    /// ```
785    ///
786    pub fn new() -> Self {
787        Color {
788            red: 0,
789            green: 0,
790            blue: 0,
791        }
792    }
793
794    /// Accessor for this color's read-only red value
795    pub fn red(&self) -> u8 {
796        self.red
797    }
798
799    /// Accessor for this color's read-only green value
800    pub fn green(&self) -> u8 {
801        self.green
802    }
803
804    /// Accessor for this color's read-only blue value
805    pub fn blue(&self) -> u8 {
806        self.blue
807    }
808}
809
810impl FromStr for Color {
811    type Err = String;
812
813    /// Create a new Color from a string slice
814    ///
815    /// Expected format is r,g,b where each value can be 0-255,
816    /// values outside this range will be converted to zero.
817    ///
818    /// Examples:
819    ///
820    /// ```
821    /// use std::str::FromStr;
822    /// use riz::models::Color;
823    ///
824    /// assert!(Color::from_str("100,80,240").is_ok());
825    /// assert!(Color::from_str("100,80,240,255").is_err());
826    /// assert!(Color::from_str("#ffeeff").is_err());
827    ///
828    /// assert_eq!(
829    ///   Color::from_str("1000,-2,256").unwrap(),
830    ///   Color::from_str("0,0,0").unwrap()
831    /// );
832    /// ```
833    ///
834    fn from_str(s: &str) -> StdResult<Self, String> {
835        let parts: Vec<_> = s.split(',').map(|c| c.parse::<u8>().unwrap_or(0)).collect();
836
837        if parts.len() == 3 {
838            Ok(Color {
839                red: parts[0],
840                green: parts[1],
841                blue: parts[2],
842            })
843        } else {
844            Err("Invalid color string".to_string())
845        }
846    }
847}
848
849/// API request for a lighting settings change on a [Light]
850#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
851pub struct LightRequest {
852    // brightness percent, valid from 10 to 100
853    // to be used with setbrightness --dim <value>
854    brightness: Option<Brightness>,
855
856    // set the rgb color value, valid from 0 to 255
857    // to be used with setrgbcolor --r <r> --g <g> --b <b>
858    color: Option<Color>,
859
860    // Color changing speed, from 20 to 200 (time %)
861    // to be used with setspeed --speed <value>
862    speed: Option<Speed>,
863
864    // Color temperature, in kelvins from 1000 to 8000
865    // to be used with setcolortemp --temp <value>
866    temp: Option<Kelvin>,
867
868    // Scene to select, from enum
869    // to be used with setscene --scene <value>
870    scene: Option<SceneMode>,
871
872    // If we would like to adjust the light's power
873    power: Option<PowerMode>,
874
875    // If we'd like to set the cool white value
876    cool: Option<White>,
877
878    // If we'd like to set the warm white value
879    warm: Option<White>,
880}
881
882impl LightRequest {
883    /// Accessor to get this request's optional [PowerMode] setting
884    pub fn power(&self) -> Option<&PowerMode> {
885        self.power.as_ref()
886    }
887}
888
889/// Describes a potential emitting state of a [Light]
890#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
891pub enum PowerMode {
892    /// Send a reboot command to the light
893    Reboot,
894
895    /// Tell the bulb to emit light
896    On,
897
898    /// Tell the bulb to stop emitting light
899    Off,
900}
901
902/// Preset lighting modes
903#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, EnumIter, PartialEq)]
904pub enum SceneMode {
905    Ocean = 1,
906    Romance = 2,
907    Sunset = 3,
908    Party = 4,
909    Fireplace = 5,
910    Cozy = 6,
911    Forest = 7,
912    PastelColors = 8,
913    WakeUp = 9,
914    Bedtime = 10,
915    WarmWhite = 11,
916    Daylight = 12,
917    CoolWhite = 13,
918    NightLight = 14,
919    Focus = 15,
920    Relax = 16,
921    TrueColors = 17,
922    TvTime = 18,
923    Plantgrowth = 19,
924    Spring = 20,
925    Summer = 21,
926    Fall = 22,
927    Deepdive = 23,
928    Jungle = 24,
929    Mojito = 25,
930    Club = 26,
931    Christmas = 27,
932    Halloween = 28,
933    Candlelight = 29,
934    GoldenWhite = 30,
935    Pulse = 31,
936    Steampunk = 32,
937    Diwali = 33,
938}
939
940impl SceneMode {
941    pub fn create(value: u8) -> Option<Self> {
942        // this is suboptimal...
943        SceneMode::iter().find(|scene| scene.clone() as u8 == value)
944    }
945}
946
947/// The last context set on the light that the API is aware of.
948///
949/// This could potentially still be wrong, the API is not the only
950/// way to change state on the bulbs, and we don't monitor/poll...
951///
952#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq)]
953pub enum LastSet {
954    /// The last set context was an RGB color
955    Color,
956
957    /// The last set context was a SceneMode
958    Scene,
959
960    /// The last set context was a Kelvin temperature
961    Temp,
962
963    /// The last set context was a cool white value
964    Cool,
965
966    /// The last set context was a warm white value
967    Warm,
968}
969
970impl LastSet {
971    fn from(value: &Payload) -> Option<Self> {
972        if value.scene.is_some() {
973            return Some(LastSet::Scene);
974        }
975        if value.get_color().is_some() {
976            return Some(LastSet::Color);
977        }
978        if value.temp.is_some() {
979            return Some(LastSet::Temp);
980        }
981        if value.cool.is_some() {
982            return Some(LastSet::Cool);
983        }
984        if value.warm.is_some() {
985            return Some(LastSet::Warm);
986        }
987        None
988    }
989}
990
991/// Tracks the last known settings set by Riz, along with the last context
992///
993/// When new settings are set, old settings that arn't overwritten are
994/// left as they were. This allows the UI to set previously set values
995/// for all potential contexts, while also displaying the active context.
996///
997#[serde_with::skip_serializing_none]
998#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
999pub struct LightStatus {
1000    /// Current color, if set
1001    color: Option<Color>,
1002
1003    /// Brightness percentage, if known
1004    brightness: Option<Brightness>,
1005
1006    /// If the bulb is emitting light
1007    emitting: bool,
1008
1009    /// Currently playing scene, if any
1010    scene: Option<SceneMode>,
1011
1012    /// Last set speed value, if known
1013    speed: Option<Speed>,
1014
1015    /// Last set light temperature, if known
1016    temp: Option<Kelvin>,
1017
1018    /// Cool white value, if known
1019    cool: Option<White>,
1020
1021    /// Warm white value, if known
1022    warm: Option<White>,
1023
1024    /// Last set value, if any
1025    last: Option<LastSet>,
1026}
1027
1028impl LightStatus {
1029    /// Accessor to get the last set context by reference
1030    pub fn last(&self) -> Option<&LastSet> {
1031        self.last.as_ref()
1032    }
1033
1034    /// Accessor to get the last set color by reference
1035    pub fn color(&self) -> Option<&Color> {
1036        self.color.as_ref()
1037    }
1038
1039    /// Accessor to get the last set brightness value by reference
1040    pub fn brightness(&self) -> Option<&Brightness> {
1041        self.brightness.as_ref()
1042    }
1043
1044    /// Accessor to get the last known light emitting state
1045    pub fn emitting(&self) -> bool {
1046        self.emitting
1047    }
1048
1049    /// Accessor to get the last set scene by reference
1050    pub fn scene(&self) -> Option<&SceneMode> {
1051        self.scene.as_ref()
1052    }
1053
1054    /// Accessor to get the last set speed value by reference
1055    pub fn speed(&self) -> Option<&Speed> {
1056        self.speed.as_ref()
1057    }
1058
1059    /// Accessor to get the last set temp value by reference
1060    pub fn temp(&self) -> Option<&Kelvin> {
1061        self.temp.as_ref()
1062    }
1063
1064    /// Accessor to get the last set cool white value by reference
1065    pub fn cool(&self) -> Option<&White> {
1066        self.cool.as_ref()
1067    }
1068
1069    /// Accessor to get the last set warm white value by reference
1070    pub fn warm(&self) -> Option<&White> {
1071        self.warm.as_ref()
1072    }
1073
1074    /// Update this status with the values from the other
1075    ///
1076    /// Any values set in other become set in self, otherwise
1077    /// values in self are left untouched.
1078    ///
1079    /// Examples:
1080    ///
1081    /// ```
1082    /// use riz::models::{LightStatus, Payload, Speed, Kelvin};
1083    ///
1084    /// let mut status = LightStatus::from(&Payload::from(&Kelvin::new()));
1085    /// assert_eq!(status.temp().unwrap().kelvin(), 1000);
1086    /// assert!(status.speed().is_none());
1087    ///
1088    /// status.update(&LightStatus::from(&Payload::from(&Speed::new())));
1089    /// assert_eq!(status.temp().unwrap().kelvin(), 1000);
1090    /// assert_eq!(status.speed().unwrap().value(), 100);
1091    /// ```
1092    ///
1093    pub fn update(&mut self, other: &Self) {
1094        if let Some(color) = &other.color {
1095            self.color = Some(color.clone());
1096        }
1097        if let Some(brightness) = &other.brightness {
1098            self.brightness = Some(brightness.clone());
1099        }
1100        self.emitting = other.emitting;
1101        self.scene = other.scene.clone();
1102        if let Some(speed) = &other.speed {
1103            self.speed = Some(speed.clone());
1104        }
1105        if let Some(temp) = &other.temp {
1106            self.temp = Some(temp.clone());
1107        }
1108        if let Some(cool) = &other.cool {
1109            self.cool = Some(cool.clone());
1110        }
1111        if let Some(warm) = &other.warm {
1112            self.warm = Some(warm.clone());
1113        }
1114        if let Some(last) = &other.last {
1115            self.last = Some(last.clone());
1116        }
1117    }
1118
1119    fn update_from_payload(&mut self, payload: &Payload) {
1120        if let Some(color) = payload.get_color() {
1121            self.color = Some(color);
1122            self.last = Some(LastSet::Color);
1123        }
1124        if let Some(dimming) = payload.dimming {
1125            self.brightness = Brightness::create(dimming);
1126        }
1127        if let Some(speed) = payload.speed {
1128            self.speed = Speed::create(speed);
1129        }
1130        if let Some(temp) = payload.temp {
1131            self.temp = Kelvin::create(temp);
1132            self.last = Some(LastSet::Temp);
1133        }
1134        if let Some(scene) = payload.scene {
1135            self.scene = SceneMode::create(scene);
1136            self.last = Some(LastSet::Scene);
1137        }
1138        if let Some(cool) = payload.cool {
1139            self.cool = White::create(cool);
1140            self.last = Some(LastSet::Cool);
1141        }
1142        if let Some(warm) = payload.warm {
1143            self.warm = White::create(warm);
1144            self.last = Some(LastSet::Warm);
1145        }
1146    }
1147
1148    fn update_from_power(&mut self, power: &PowerMode) {
1149        match power {
1150            PowerMode::Off => self.emitting = false,
1151            _ => self.emitting = true,
1152        }
1153    }
1154}
1155
1156impl From<&Payload> for LightStatus {
1157    fn from(payload: &Payload) -> Self {
1158        let color = payload.get_color();
1159
1160        let brightness = if let Some(value) = payload.dimming {
1161            Brightness::create(value)
1162        } else {
1163            None
1164        };
1165
1166        let scene = if let Some(scene) = payload.scene {
1167            SceneMode::create(scene)
1168        } else {
1169            None
1170        };
1171
1172        let speed = if let Some(speed) = payload.speed {
1173            Speed::create(speed)
1174        } else {
1175            None
1176        };
1177
1178        let temp = if let Some(temp) = payload.temp {
1179            Kelvin::create(temp)
1180        } else {
1181            None
1182        };
1183
1184        let cool = if let Some(cool) = payload.cool {
1185            White::create(cool)
1186        } else {
1187            None
1188        };
1189
1190        let warm = if let Some(warm) = payload.warm {
1191            White::create(warm)
1192        } else {
1193            None
1194        };
1195
1196        LightStatus {
1197            color,
1198            brightness,
1199            emitting: true, // we don't actually know this here...
1200            scene,
1201            speed,
1202            temp,
1203            cool,
1204            warm,
1205            last: LastSet::from(payload),
1206        }
1207    }
1208}
1209
1210impl From<&PowerMode> for LightStatus {
1211    fn from(power: &PowerMode) -> Self {
1212        LightStatus {
1213            color: None,
1214            brightness: None,
1215            emitting: !matches!(power, PowerMode::Off),
1216            scene: None,
1217            speed: None,
1218            temp: None,
1219            cool: None,
1220            warm: None,
1221            last: None,
1222        }
1223    }
1224}
1225
1226impl From<&BulbStatus> for LightStatus {
1227    fn from(bulb: &BulbStatus) -> Self {
1228        let res = &bulb.result;
1229
1230        LightStatus {
1231            color: res.get_color(),
1232            brightness: Brightness::create(res.dimming.unwrap_or(0)),
1233            cool: White::create(res.cool.unwrap_or(0)),
1234            warm: White::create(res.warm.unwrap_or(0)),
1235            emitting: res.emitting,
1236            scene: SceneMode::create(res.scene),
1237            // NB: these are not returned from getPilot...
1238            //     best we can do is track what we set then
1239            speed: None,
1240            temp: None,
1241            last: None,
1242        }
1243    }
1244}
1245
1246/// Bulb status, as reported by the bulb.
1247///
1248/// Several lighting settings are available as settings, but we can't
1249/// get the state back out of the bulb.
1250///
1251/// BulbStatus is *only* what the bulb reports, it is then merged into a
1252/// [LightStatus] which adds the logic to track settings the bulb will
1253/// accept but not report.
1254#[derive(Debug, Serialize, Deserialize, Clone)]
1255struct BulbStatus {
1256    env: String,
1257    method: String,
1258    result: BulbStatusResult,
1259}
1260
1261#[derive(Debug, Serialize, Deserialize, Clone)]
1262struct BulbStatusResult {
1263    /// red (0-255)
1264    #[serde(rename = "r")]
1265    red: Option<u8>,
1266
1267    /// green (0-255)
1268    #[serde(rename = "g")]
1269    green: Option<u8>,
1270
1271    /// blue (0-255)
1272    #[serde(rename = "b")]
1273    blue: Option<u8>,
1274
1275    /// dimming percent (0-100)
1276    dimming: Option<u8>,
1277
1278    /// bulb wifi mac address
1279    mac: String,
1280
1281    /// true when bulb state is on
1282    #[serde(rename = "state")]
1283    emitting: bool,
1284
1285    /// current scene ID, zero if not playing a scene
1286    #[serde(rename = "sceneId")]
1287    scene: u8,
1288
1289    /// bulb's wifi signal strength
1290    rssi: i32,
1291
1292    /// bulb's cool white value
1293    #[serde(rename = "c")]
1294    cool: Option<u8>,
1295
1296    /// bulb's warm white value
1297    #[serde(rename = "w")]
1298    warm: Option<u8>,
1299}
1300
1301impl BulbStatusResult {
1302    fn get_color(&self) -> Option<Color> {
1303        if let (Some(red), Some(green), Some(blue)) = (self.red, self.green, self.blue) {
1304            Some(Color { red, green, blue })
1305        } else {
1306            None
1307        }
1308    }
1309}
1310
1311/// Response which could alter the state of a [Light]
1312///
1313/// Used with [Light::process_reply] or [Room::process_reply]. Or use
1314/// [crate::Storage::process_reply] to also update the `rooms.json`
1315///
1316#[derive(Debug)]
1317pub struct LightingResponse {
1318    ip: Ipv4Addr,
1319    response: LightingResponseType,
1320}
1321
1322impl LightingResponse {
1323    /// Create a [LightingResponse] for a [Ipv4Addr] from a [Payload]
1324    pub fn payload(ip: Ipv4Addr, payload: Payload) -> Self {
1325        LightingResponse {
1326            ip,
1327            response: LightingResponseType::Payload(payload),
1328        }
1329    }
1330
1331    /// Create a [LightingResponse] for a [Ipv4Addr] from a [PowerMode]
1332    pub fn power(ip: Ipv4Addr, power: PowerMode) -> Self {
1333        LightingResponse {
1334            ip,
1335            response: LightingResponseType::Power(power),
1336        }
1337    }
1338
1339    /// Create a [LightingResponse] for a [Ipv4Addr] from a [LightStatus]
1340    pub fn status(ip: Ipv4Addr, status: LightStatus) -> Self {
1341        LightingResponse {
1342            ip,
1343            response: LightingResponseType::Status(status),
1344        }
1345    }
1346}
1347
1348/// Reply path payload details for modifying [Light] state
1349#[derive(Debug)]
1350pub enum LightingResponseType {
1351    /// Response from any lighting setting change
1352    Payload(Payload),
1353
1354    /// Response from any power (emitting) setting change
1355    Power(PowerMode),
1356
1357    /// Response from a bulb status fetch
1358    Status(LightStatus),
1359}
1360
1361/// JSON payload to send at Wiz lights to modify their settings
1362///
1363/// You can create a singular payload by using one of the [From] trait
1364/// implementations. Or create a new empty payload and add attributes to
1365/// it with the helper methods.
1366///
1367#[serde_with::skip_serializing_none]
1368#[derive(Default, Debug, Serialize, Deserialize, Clone)]
1369pub struct Payload {
1370    #[serde(rename = "sceneId")]
1371    scene: Option<u8>,
1372
1373    dimming: Option<u8>,
1374    speed: Option<u8>,
1375    temp: Option<u16>,
1376
1377    #[serde(rename = "r")]
1378    red: Option<u8>,
1379    #[serde(rename = "g")]
1380    green: Option<u8>,
1381    #[serde(rename = "b")]
1382    blue: Option<u8>,
1383
1384    #[serde(rename = "c")]
1385    cool: Option<u8>,
1386    #[serde(rename = "w")]
1387    warm: Option<u8>,
1388}
1389
1390impl Payload {
1391    /// Create a new blank payload
1392    ///
1393    /// Note that at least one helper method must be called if creating a
1394    /// payload this way, or the payload will be invalid and cause an error
1395    /// if you try to use it with a [Light::set] call.
1396    ///
1397    /// You can stack as many modes in a single call as you want. The light
1398    /// will determine if it can set that combination of settings. And if it
1399    /// can't, will make a best effort to set something close.
1400    ///
1401    /// # Examples
1402    ///
1403    /// ```
1404    /// use riz::models::Payload;
1405    ///
1406    /// let mut payload = Payload::new();
1407    /// assert_eq!(payload.is_valid(), false);
1408    /// ```
1409    ///
1410    pub fn new() -> Self {
1411        Payload {
1412            scene: None,
1413            dimming: None,
1414            speed: None,
1415            temp: None,
1416            red: None,
1417            green: None,
1418            blue: None,
1419            cool: None,
1420            warm: None,
1421        }
1422    }
1423
1424    /// Checks if this payload is valid
1425    ///
1426    /// Note that speed is not valid on it's own, it must be set with a
1427    /// scene mode as well (Wiz limitation).
1428    ///
1429    /// # Examples
1430    ///
1431    /// ```
1432    /// use riz::models::{Payload, SceneMode, Speed};
1433    ///
1434    /// let mut payload = Payload::new();
1435    ///
1436    /// payload.speed(&Speed::create(100).unwrap());
1437    /// assert_eq!(payload.is_valid(), false);
1438    ///
1439    /// payload.scene(&SceneMode::Focus);
1440    /// assert_eq!(payload.is_valid(), true);
1441    /// ```
1442    ///
1443    pub fn is_valid(&self) -> bool {
1444        self.scene.is_some()
1445            || self.dimming.is_some()
1446            || self.temp.is_some()
1447            || (self.red.is_some() && self.green.is_some() && self.blue.is_some())
1448            || self.cool.is_some()
1449            || self.warm.is_some()
1450    }
1451
1452    /// Set the SceneMode to use in this payload, by reference
1453    ///
1454    /// # Examples
1455    ///
1456    /// ```
1457    /// use riz::models::{Payload, SceneMode};
1458    ///
1459    /// let mut payload = Payload::new();
1460    /// payload.scene(&SceneMode::Focus);
1461    /// assert_eq!(payload.is_valid(), true);
1462    /// ```
1463    ///
1464    pub fn scene(&mut self, scene: &SceneMode) {
1465        self.scene = Some(scene.clone() as u8);
1466    }
1467
1468    /// Set the Brightness value in this payload.
1469    ///
1470    /// Note that brightness can be applied to any context,
1471    /// as long as the bulb is emitting.
1472    ///
1473    /// # Examples
1474    ///
1475    /// ```
1476    /// use riz::models::{Payload, Brightness};
1477    ///
1478    /// let mut payload = Payload::new();
1479    /// payload.brightness(&Brightness::create(100).unwrap());
1480    /// assert_eq!(payload.is_valid(), true);
1481    /// ```
1482    ///
1483    pub fn brightness(&mut self, brightness: &Brightness) {
1484        self.dimming = Some(brightness.value);
1485    }
1486
1487    /// Set the speed value in this payload, by reference
1488    ///
1489    /// Speed is only relevant when also setting a SceneMode.
1490    /// If speed is sent with other attributes and not a scene,
1491    /// the other attributes will set the context on the bulb.
1492    /// However, if you also use the payload to update state,
1493    /// the speed value will still be reflected in the light's
1494    /// last known status.
1495    ///
1496    /// # Examples
1497    ///
1498    /// ```
1499    /// use std::net::Ipv4Addr;
1500    /// use std::str::FromStr;
1501    /// use riz::models::{Light, Payload, LastSet, Color, Speed, LightingResponse};
1502    ///
1503    /// let ip = Ipv4Addr::from_str("10.1.2.3").unwrap();
1504    /// let mut light = Light::new(ip, None);
1505    ///
1506    /// let mut payload = Payload::new();
1507    /// payload.speed(&Speed::create(100).unwrap());
1508    /// payload.color(&Color::from_str("0,0,255").unwrap());
1509    ///
1510    /// let resp = LightingResponse::payload(ip, payload);
1511    /// assert!(light.process_reply(&resp));
1512    ///
1513    /// let status = light.status().unwrap();
1514    /// assert_eq!(status.last().unwrap(), &LastSet::Color);
1515    /// assert_eq!(status.speed().unwrap().value(), 100);
1516    /// ```
1517    ///
1518    pub fn speed(&mut self, speed: &Speed) {
1519        self.speed = Some(speed.value);
1520    }
1521
1522    /// Set the temperature value in this payload, by reference
1523    ///
1524    /// Note that it is not possible to retrieve this temperature value
1525    /// back from the bulb itself. Last known settings for this value are
1526    /// from storing the state after each set call only.
1527    ///
1528    /// # Examples
1529    ///
1530    /// ```
1531    /// use riz::models::{Payload, Kelvin};
1532    ///
1533    /// let mut payload = Payload::new();
1534    /// payload.temp(&Kelvin::create(4000).unwrap());
1535    /// assert_eq!(payload.is_valid(), true);
1536    /// ```
1537    ///
1538    pub fn temp(&mut self, temp: &Kelvin) {
1539        self.temp = Some(temp.kelvin);
1540    }
1541
1542    /// Set the RGB color mode in this payload, by reference
1543    ///
1544    /// # Examples
1545    ///
1546    /// ```
1547    /// use std::str::FromStr;
1548    /// use riz::models::{Payload, Color};
1549    ///
1550    /// let mut payload = Payload::new();
1551    /// payload.color(&Color::from_str("255,255,255").unwrap());
1552    /// assert_eq!(payload.is_valid(), true);
1553    /// ```
1554    ///
1555    pub fn color(&mut self, color: &Color) {
1556        self.red = Some(color.red);
1557        self.green = Some(color.green);
1558        self.blue = Some(color.blue);
1559    }
1560
1561    /// Set the cool white value in this payload, by reference
1562    ///
1563    /// This can be used on it's own, some scenes might also use it
1564    ///
1565    /// # Examples
1566    ///
1567    /// ```
1568    /// use riz::models::{Payload, White};
1569    ///
1570    /// let mut payload = Payload::new();
1571    /// payload.cool(&White::create(50).unwrap());
1572    /// assert_eq!(payload.is_valid(), true);
1573    /// ```
1574    ///
1575    pub fn cool(&mut self, cool: &White) {
1576        self.cool = Some(cool.value);
1577    }
1578
1579    /// Set the warm white value in this payload, by reference
1580    ///
1581    /// This can be used on it's own, some scenes might also use it
1582    ///
1583    /// # Examples
1584    ///
1585    /// ```
1586    /// use riz::models::{Payload, White};
1587    ///
1588    /// let mut payload = Payload::new();
1589    /// payload.warm(&White::create(50).unwrap());
1590    /// assert_eq!(payload.is_valid(), true);
1591    /// ```
1592    ///
1593    pub fn warm(&mut self, warm: &White) {
1594        self.warm = Some(warm.value);
1595    }
1596
1597    /// Helper method to create a color when we have one set
1598    fn get_color(&self) -> Option<Color> {
1599        if let (Some(red), Some(green), Some(blue)) = (self.red, self.green, self.blue) {
1600            Some(Color { red, green, blue })
1601        } else {
1602            None
1603        }
1604    }
1605}
1606
1607impl From<&SceneMode> for Payload {
1608    fn from(scene: &SceneMode) -> Self {
1609        let mut p = Payload::new();
1610        p.scene(scene);
1611        p
1612    }
1613}
1614
1615impl From<&Kelvin> for Payload {
1616    fn from(kelvin: &Kelvin) -> Self {
1617        let mut p = Payload::new();
1618        p.temp(kelvin);
1619        p
1620    }
1621}
1622
1623impl From<&Color> for Payload {
1624    fn from(color: &Color) -> Self {
1625        let mut p = Payload::new();
1626        p.color(color);
1627        p
1628    }
1629}
1630
1631impl From<&Speed> for Payload {
1632    fn from(speed: &Speed) -> Self {
1633        let mut p = Payload::new();
1634        p.speed(speed);
1635        p
1636    }
1637}
1638
1639impl From<&LightRequest> for Payload {
1640    fn from(req: &LightRequest) -> Self {
1641        let mut p = Payload::new();
1642        if let Some(brightness) = &req.brightness {
1643            p.brightness(brightness);
1644        }
1645        if let Some(color) = &req.color {
1646            p.color(color);
1647        }
1648        if let Some(speed) = &req.speed {
1649            p.speed(speed);
1650        }
1651        if let Some(temp) = &req.temp {
1652            p.temp(temp);
1653        }
1654        if let Some(scene) = &req.scene {
1655            p.scene(scene);
1656        }
1657        if let Some(cool) = &req.cool {
1658            p.cool(cool);
1659        }
1660        if let Some(warm) = &req.warm {
1661            p.warm(warm);
1662        }
1663        p
1664    }
1665}
1666
1667impl From<&Brightness> for Payload {
1668    fn from(brightness: &Brightness) -> Self {
1669        let mut p = Payload::new();
1670        p.brightness(brightness);
1671        p
1672    }
1673}