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}