lifx_core/
lib.rs

1//! This crate provides low-level message types and structures for dealing with the LIFX LAN protocol.
2//!
3//! This lets you control lights on your local area network.  More info can be found here:
4//! <https://lan.developer.lifx.com/>
5//!
6//! Since this is a low-level library, it does not deal with issues like talking to the network,
7//! caching light state, or waiting for replies.  This should be done at a higher-level library.
8//!
9//! # Discovery
10//!
11//! To discover lights on your LAN, send a [Message::GetService] message as a UDP broadcast to port 56700.
12//! When a device is discovered, the [Service] types and IP port are provided.  To get additional
13//! info about each device, send additional Get messages directly to each device (by setting the
14//! [FrameAddress::target] field to the bulbs target ID, and then send a UDP packet to the IP address
15//! associated with the device).
16//!
17//! # Reserved fields
18//! When *constructing* packets, you must always set every reserved field to zero.  However, it's
19//! possible to receive packets with these fields set to non-zero values.  Be conservative in what
20//! you send, and liberal in what you accept.
21//!
22//! # Unknown values
23//! It's common to see packets for LIFX bulbs that don't match the documented protocol.  These are
24//! suspected to be internal messages that are used by official LIFX apps, but that aren't documented.
25
26use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
27use std::cmp::PartialEq;
28use std::convert::{TryFrom, TryInto};
29use std::ffi::{CStr, CString};
30use std::io;
31use std::io::Cursor;
32use thiserror::Error;
33
34#[cfg(fuzzing)]
35#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
36#[derive(Debug, Clone)]
37pub struct ComparableFloat(f32);
38#[cfg(fuzzing)]
39impl PartialEq for ComparableFloat {
40    fn eq(&self, other: &Self) -> bool {
41        if self.0.is_nan() && other.0.is_nan() {
42            true
43        } else {
44            self.0 == other.0
45        }
46    }
47}
48#[cfg(fuzzing)]
49impl From<f32> for ComparableFloat {
50    fn from(f: f32) -> Self {
51        ComparableFloat(f)
52    }
53}
54
55/// Various message encoding/decoding errors
56#[derive(Error, Debug)]
57pub enum Error {
58    /// This error means we were unable to parse a raw message because its type is unknown.
59    ///
60    /// LIFX devices are known to send messages that are not officially documented, so this error
61    /// type does not necessarily represent a bug.
62    #[error("unknown message type: `{0}`")]
63    UnknownMessageType(u16),
64    /// This error means one of the message fields contains an invalid or unsupported value.
65    #[error("protocol error: `{0}`")]
66    ProtocolError(String),
67
68    #[error("i/o error")]
69    Io(#[from] io::Error),
70}
71
72impl From<std::convert::Infallible> for Error {
73    fn from(_: std::convert::Infallible) -> Self {
74        unreachable!()
75    }
76}
77
78impl TryFrom<u8> for ApplicationRequest {
79    type Error = Error;
80    fn try_from(val: u8) -> Result<ApplicationRequest, Error> {
81        match val {
82            0 => Ok(ApplicationRequest::NoApply),
83            1 => Ok(ApplicationRequest::Apply),
84            2 => Ok(ApplicationRequest::ApplyOnly),
85            x => Err(Error::ProtocolError(format!(
86                "Unknown application request {}",
87                x
88            ))),
89        }
90    }
91}
92
93impl TryFrom<u8> for Waveform {
94    type Error = Error;
95    fn try_from(val: u8) -> Result<Waveform, Error> {
96        match val {
97            0 => Ok(Waveform::Saw),
98            1 => Ok(Waveform::Sine),
99            2 => Ok(Waveform::HalfSign),
100            3 => Ok(Waveform::Triangle),
101            4 => Ok(Waveform::Pulse),
102            x => Err(Error::ProtocolError(format!(
103                "Unknown waveform value {}",
104                x
105            ))),
106        }
107    }
108}
109
110impl TryFrom<u8> for Service {
111    type Error = Error;
112    fn try_from(val: u8) -> Result<Service, Error> {
113        match val {
114            x if x == Service::UDP as u8 => Ok(Service::UDP),
115            x if x == Service::Reserved1 as u8 => Ok(Service::Reserved1),
116            x if x == Service::Reserved2 as u8 => Ok(Service::Reserved2),
117            x if x == Service::Reserved3 as u8 => Ok(Service::Reserved3),
118            x if x == Service::Reserved4 as u8 => Ok(Service::Reserved4),
119            val => Err(Error::ProtocolError(format!(
120                "Unknown service value {}",
121                val
122            ))),
123        }
124    }
125}
126
127impl TryFrom<u16> for PowerLevel {
128    type Error = Error;
129    fn try_from(val: u16) -> Result<PowerLevel, Error> {
130        match val {
131            x if x == PowerLevel::Enabled as u16 => Ok(PowerLevel::Enabled),
132            x if x == PowerLevel::Standby as u16 => Ok(PowerLevel::Standby),
133            x => Err(Error::ProtocolError(format!("Unknown power level {}", x))),
134        }
135    }
136}
137
138#[derive(Copy, Clone, PartialEq, Eq)]
139#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
140pub struct EchoPayload(pub [u8; 64]);
141
142impl std::fmt::Debug for EchoPayload {
143    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
144        write!(f, "<EchoPayload>")
145    }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
150pub struct LifxIdent(pub [u8; 16]);
151
152/// Lifx strings are fixed-length (32-bytes maximum)
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct LifxString(CString);
155
156impl LifxString {
157    /// Constructs a new LifxString, truncating to 32 characters and ensuring there's a null terminator
158    pub fn new(s: &CStr) -> LifxString {
159        let mut b = s.to_bytes().to_vec();
160        if b.len() > 31 {
161            b[31] = 0;
162            let b = b[..32].to_vec();
163            LifxString(unsafe {
164                // Safety: we created the null terminator above, and the rest of the bytes originally came from a CStr
165                CString::from_vec_with_nul_unchecked(b)
166            })
167        } else {
168            LifxString(s.to_owned())
169        }
170    }
171    pub fn cstr(&self) -> &CStr {
172        &self.0
173    }
174}
175
176impl std::fmt::Display for LifxString {
177    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
178        write!(fmt, "{}", self.0.to_string_lossy())
179    }
180}
181
182impl std::cmp::PartialEq<str> for LifxString {
183    fn eq(&self, other: &str) -> bool {
184        self.0.to_string_lossy() == other
185    }
186}
187
188#[cfg(feature = "arbitrary")]
189impl<'a> arbitrary::Arbitrary<'a> for LifxString {
190    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
191        // first pick a random length, between 0 and 32
192        let len: usize = u.int_in_range(0..=31)?;
193
194        let mut v = Vec::new();
195        for _ in 0..len {
196            let b: std::num::NonZeroU8 = u.arbitrary()?;
197            v.push(b);
198        }
199
200        let s = CString::from(v);
201        assert!(s.to_bytes_with_nul().len() <= 32);
202        Ok(LifxString(s))
203    }
204}
205
206trait LittleEndianWriter<T>: WriteBytesExt {
207    fn write_val(&mut self, v: T) -> Result<(), io::Error>;
208}
209
210macro_rules! derive_writer {
211{ $( $m:ident: $t:ty ),*} => {
212    $(
213        impl<T: WriteBytesExt> LittleEndianWriter<$t> for T {
214            fn write_val(&mut self, v: $t) -> Result<(), io::Error> {
215                self . $m ::<LittleEndian>(v)
216            }
217        }
218    )*
219
220}
221}
222
223derive_writer! { write_u32: u32, write_u16: u16, write_i16: i16, write_u64: u64, write_f32: f32 }
224
225#[cfg(fuzzing)]
226impl<T: WriteBytesExt> LittleEndianWriter<ComparableFloat> for T {
227    fn write_val(&mut self, v: ComparableFloat) -> Result<(), io::Error> {
228        self.write_f32::<LittleEndian>(v.0)
229    }
230}
231
232impl<T: WriteBytesExt> LittleEndianWriter<u8> for T {
233    fn write_val(&mut self, v: u8) -> Result<(), io::Error> {
234        self.write_u8(v)
235    }
236}
237
238impl<T: WriteBytesExt> LittleEndianWriter<bool> for T {
239    fn write_val(&mut self, v: bool) -> Result<(), io::Error> {
240        self.write_u8(if v { 1 } else { 0 })
241    }
242}
243
244impl<T> LittleEndianWriter<LifxString> for T
245where
246    T: WriteBytesExt,
247{
248    fn write_val(&mut self, v: LifxString) -> Result<(), io::Error> {
249        let b = v.0.to_bytes();
250        for idx in 0..32 {
251            if idx >= b.len() {
252                self.write_u8(0)?;
253            } else {
254                self.write_u8(b[idx])?;
255            }
256        }
257        Ok(())
258    }
259}
260
261impl<T> LittleEndianWriter<LifxIdent> for T
262where
263    T: WriteBytesExt,
264{
265    fn write_val(&mut self, v: LifxIdent) -> Result<(), io::Error> {
266        for idx in 0..16 {
267            self.write_u8(v.0[idx])?;
268        }
269        Ok(())
270    }
271}
272
273impl<T> LittleEndianWriter<EchoPayload> for T
274where
275    T: WriteBytesExt,
276{
277    fn write_val(&mut self, v: EchoPayload) -> Result<(), io::Error> {
278        for idx in 0..64 {
279            self.write_u8(v.0[idx])?;
280        }
281        Ok(())
282    }
283}
284
285impl<T> LittleEndianWriter<HSBK> for T
286where
287    T: WriteBytesExt,
288{
289    fn write_val(&mut self, v: HSBK) -> Result<(), io::Error> {
290        self.write_val(v.hue)?;
291        self.write_val(v.saturation)?;
292        self.write_val(v.brightness)?;
293        self.write_val(v.kelvin)?;
294        Ok(())
295    }
296}
297
298impl<T> LittleEndianWriter<PowerLevel> for T
299where
300    T: WriteBytesExt,
301{
302    fn write_val(&mut self, v: PowerLevel) -> Result<(), io::Error> {
303        self.write_u16::<LittleEndian>(v as u16)
304    }
305}
306
307impl<T> LittleEndianWriter<ApplicationRequest> for T
308where
309    T: WriteBytesExt,
310{
311    fn write_val(&mut self, v: ApplicationRequest) -> Result<(), io::Error> {
312        self.write_u8(v as u8)
313    }
314}
315
316impl<T> LittleEndianWriter<Waveform> for T
317where
318    T: WriteBytesExt,
319{
320    fn write_val(&mut self, v: Waveform) -> Result<(), io::Error> {
321        self.write_u8(v as u8)
322    }
323}
324
325impl<T> LittleEndianWriter<LastHevCycleResult> for T
326where
327    T: WriteBytesExt,
328{
329    fn write_val(&mut self, v: LastHevCycleResult) -> Result<(), io::Error> {
330        self.write_u8(v as u8)
331    }
332}
333
334impl<T> LittleEndianWriter<MultiZoneEffectType> for T
335where
336    T: WriteBytesExt,
337{
338    fn write_val(&mut self, v: MultiZoneEffectType) -> Result<(), io::Error> {
339        self.write_u8(v as u8)
340    }
341}
342
343impl<T> LittleEndianWriter<&Box<[HSBK; 82]>> for T
344where
345    T: WriteBytesExt,
346{
347    fn write_val(&mut self, v: &Box<[HSBK; 82]>) -> Result<(), io::Error> {
348        for elem in &**v {
349            self.write_val(*elem)?;
350        }
351        Ok(())
352    }
353}
354
355impl<T> LittleEndianWriter<&[u8; 32]> for T
356where
357    T: WriteBytesExt,
358{
359    fn write_val(&mut self, v: &[u8; 32]) -> Result<(), io::Error> {
360        self.write_all(v)
361    }
362}
363
364impl<T> LittleEndianWriter<&[u32; 8]> for T
365where
366    T: WriteBytesExt,
367{
368    fn write_val(&mut self, v: &[u32; 8]) -> Result<(), io::Error> {
369        for elem in v {
370            self.write_u32::<LittleEndian>(*elem)?;
371        }
372        Ok(())
373    }
374}
375
376trait LittleEndianReader<T> {
377    fn read_val(&mut self) -> Result<T, io::Error>;
378}
379
380macro_rules! derive_reader {
381{ $( $m:ident: $t:ty ),*} => {
382    $(
383        impl<T: ReadBytesExt> LittleEndianReader<$t> for T {
384            fn read_val(&mut self) -> Result<$t, io::Error> {
385                self . $m ::<LittleEndian>()
386            }
387        }
388    )*
389
390}
391}
392
393derive_reader! { read_u32: u32, read_u16: u16, read_i16: i16, read_u64: u64, read_f32: f32 }
394
395impl<R: ReadBytesExt> LittleEndianReader<u8> for R {
396    fn read_val(&mut self) -> Result<u8, io::Error> {
397        self.read_u8()
398    }
399}
400
401impl<R: ReadBytesExt> LittleEndianReader<bool> for R {
402    fn read_val(&mut self) -> Result<bool, io::Error> {
403        Ok(self.read_u8()? > 0)
404    }
405}
406
407impl<R: ReadBytesExt> LittleEndianReader<LastHevCycleResult> for R {
408    fn read_val(&mut self) -> Result<LastHevCycleResult, io::Error> {
409        let val: u8 = self.read_val()?;
410        match val {
411            0 => Ok(LastHevCycleResult::Success),
412            1 => Ok(LastHevCycleResult::Busy),
413            2 => Ok(LastHevCycleResult::InterruptedByReset),
414            3 => Ok(LastHevCycleResult::InterruptedByHomekit),
415            4 => Ok(LastHevCycleResult::InterruptedByLan),
416            5 => Ok(LastHevCycleResult::InterruptedByCloud),
417            _ => Ok(LastHevCycleResult::None),
418        }
419    }
420}
421
422impl<R: ReadBytesExt> LittleEndianReader<MultiZoneEffectType> for R {
423    fn read_val(&mut self) -> Result<MultiZoneEffectType, io::Error> {
424        let val: u8 = self.read_val()?;
425        match val {
426            0 => Ok(MultiZoneEffectType::Off),
427            1 => Ok(MultiZoneEffectType::Move),
428            2 => Ok(MultiZoneEffectType::Reserved1),
429            _ => Ok(MultiZoneEffectType::Reserved2),
430        }
431    }
432}
433
434impl<R: ReadBytesExt> LittleEndianReader<[u8; 32]> for R {
435    fn read_val(&mut self) -> Result<[u8; 32], io::Error> {
436        let mut data = [0; 32];
437        self.read_exact(&mut data)?;
438        Ok(data)
439    }
440}
441
442impl<R: ReadBytesExt> LittleEndianReader<[u32; 8]> for R {
443    fn read_val(&mut self) -> Result<[u32; 8], io::Error> {
444        let mut data = [0; 8];
445        for x in &mut data {
446            *x = self.read_u32::<LittleEndian>()?;
447        }
448        Ok(data)
449    }
450}
451
452impl<R: ReadBytesExt> LittleEndianReader<[HSBK; 82]> for R {
453    fn read_val(&mut self) -> Result<[HSBK; 82], io::Error> {
454        let mut data = [HSBK {
455            hue: 0,
456            saturation: 0,
457            brightness: 0,
458            kelvin: 0,
459        }; 82];
460        for x in &mut data {
461            *x = self.read_val()?;
462        }
463
464        Ok(data)
465    }
466}
467
468impl<R: ReadBytesExt> LittleEndianReader<HSBK> for R {
469    fn read_val(&mut self) -> Result<HSBK, io::Error> {
470        let hue = self.read_val()?;
471        let sat = self.read_val()?;
472        let bri = self.read_val()?;
473        let kel = self.read_val()?;
474        Ok(HSBK {
475            hue,
476            saturation: sat,
477            brightness: bri,
478            kelvin: kel,
479        })
480    }
481}
482
483impl<R: ReadBytesExt> LittleEndianReader<LifxIdent> for R {
484    fn read_val(&mut self) -> Result<LifxIdent, io::Error> {
485        let mut val = [0; 16];
486        for v in &mut val {
487            *v = self.read_val()?;
488        }
489        Ok(LifxIdent(val))
490    }
491}
492
493impl<R: ReadBytesExt> LittleEndianReader<LifxString> for R {
494    fn read_val(&mut self) -> Result<LifxString, io::Error> {
495        let mut bytes = Vec::new();
496        for _ in 0..31 {
497            let c: u8 = self.read_val()?;
498            if let Some(b) = std::num::NonZeroU8::new(c) {
499                bytes.push(b);
500            }
501        }
502        // read the null terminator
503        self.read_u8()?;
504
505        Ok(LifxString(CString::from(bytes)))
506    }
507}
508
509impl<R: ReadBytesExt> LittleEndianReader<EchoPayload> for R {
510    fn read_val(&mut self) -> Result<EchoPayload, io::Error> {
511        let mut val = [0; 64];
512        for v in val.iter_mut() {
513            *v = self.read_val()?;
514        }
515        Ok(EchoPayload(val))
516    }
517}
518
519impl<R: ReadBytesExt> LittleEndianReader<PowerLevel> for R {
520    fn read_val(&mut self) -> Result<PowerLevel, io::Error> {
521        let val: u16 = self.read_val()?;
522        if val == 0 {
523            Ok(PowerLevel::Standby)
524        } else {
525            Ok(PowerLevel::Enabled)
526        }
527    }
528}
529
530impl<R: ReadBytesExt> LittleEndianReader<Waveform> for R {
531    fn read_val(&mut self) -> Result<Waveform, io::Error> {
532        let v = self.read_u8()?;
533        match v {
534            0 => Ok(Waveform::Saw),
535            1 => Ok(Waveform::Sine),
536            2 => Ok(Waveform::HalfSign),
537            3 => Ok(Waveform::Triangle),
538            4 => Ok(Waveform::Pulse),
539            _ => Ok(Waveform::Saw), // default
540        }
541    }
542}
543
544macro_rules! unpack {
545    ($msg:ident, $typ:ident, $( $n:ident: $t:ty ),*) => {
546        {
547        let mut c = Cursor::new(&$msg.payload);
548        $(
549            let $n: $t = c.read_val()?;
550        )*
551
552            Message::$typ {
553            $(
554                    $n: $n.try_into()?,
555            )*
556        }
557        }
558    };
559}
560
561//trace_macros!(true);
562//message_types! {
563//    /// GetService - 2
564//    ///
565//    /// Sent by a client to acquire responses from all devices on the local network.
566//    GetService(2, ),
567//    /// StateService - 3
568//    ///
569//    /// Response to GetService message.  Provides the device Service and Port.  If the Service
570//    /// is temporarily unavailable, then the port value will be zero
571//    StateService(3, {
572//        service: Service,
573//        port: u32
574//    })
575//}
576//trace_macros!(false);
577
578/// What services are exposed by the device.
579///
580/// LIFX only documents the UDP service, though bulbs may support other undocumented services.
581/// Since these other services are unsupported by the lifx-core library, a message with a non-UDP
582/// service cannot be constructed.
583#[repr(u8)]
584#[derive(Debug, Copy, Clone, PartialEq, Eq)]
585#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
586pub enum Service {
587    UDP = 1,
588    Reserved1 = 2,
589    Reserved2 = 3,
590    Reserved3 = 4,
591    Reserved4 = 5,
592}
593
594#[repr(u16)]
595#[derive(Debug, Copy, Clone, PartialEq, Eq)]
596#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
597pub enum PowerLevel {
598    Standby = 0,
599    Enabled = 65535,
600}
601
602/// Controls how/when multizone devices apply color changes
603///
604/// See also [Message::SetColorZones].
605#[repr(u8)]
606#[derive(Debug, Copy, Clone, PartialEq, Eq)]
607#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
608pub enum ApplicationRequest {
609    /// Don't apply the requested changes until a message with Apply or ApplyOnly is sent
610    NoApply = 0,
611    /// Apply the changes immediately and apply any pending changes
612    Apply = 1,
613    /// Ignore the requested changes in this message and only apply pending changes
614    ApplyOnly = 2,
615}
616
617#[repr(u8)]
618#[derive(Debug, Copy, Clone, PartialEq, Eq)]
619#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
620pub enum Waveform {
621    Saw = 0,
622    Sine = 1,
623    HalfSign = 2,
624    Triangle = 3,
625    Pulse = 4,
626}
627
628#[repr(u8)]
629#[derive(Debug, Copy, Clone, PartialEq, Eq)]
630#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
631pub enum LastHevCycleResult {
632    Success = 0,
633    Busy = 1,
634    InterruptedByReset = 2,
635    InterruptedByHomekit = 3,
636    InterruptedByLan = 4,
637    InterruptedByCloud = 5,
638    None = 255,
639}
640
641#[repr(u8)]
642#[derive(Debug, Copy, Clone, PartialEq, Eq)]
643#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
644pub enum MultiZoneEffectType {
645    Off = 0,
646    Move = 1,
647    Reserved1 = 2,
648    Reserved2 = 3,
649}
650
651/// Decoded LIFX Messages
652///
653/// This enum lists all of the LIFX message types known to this library.
654///
655/// Note that other message types exist, but are not officially documented (and so are not
656/// available here).
657#[derive(Clone, Debug, PartialEq)]
658#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
659pub enum Message {
660    /// Sent by a client to acquire responses from all devices on the local network. No payload is
661    /// required. Causes the devices to transmit a [Message::StateService] message.
662    ///
663    /// Message type 2
664    GetService,
665
666    /// Response to [Message::GetService] message.
667    ///
668    /// You'll want to save the port number in this message, so you can send future messages directly
669    /// to this device.
670    ///
671    /// Message type 3
672    StateService {
673        /// unsigned 8-bit integer, maps to `Service`
674        service: Service,
675        /// Port number of the light.  If the service is temporarily unavailable, then the port value
676        /// will be 0.
677        port: u32,
678    },
679
680    /// Get Host MCU information. No payload is required. Causes the device to transmit a
681    /// [Message::StateHostInfo] message.
682    ///
683    /// Message type 12
684    GetHostInfo,
685
686    /// Response to [Message::GetHostInfo] message.
687    ///
688    /// Provides host MCU information.
689    ///
690    /// Message type 13
691    StateHostInfo {
692        /// radio receive signal strength in milliWatts
693        #[cfg(not(fuzzing))]
694        signal: f32,
695        #[cfg(fuzzing)]
696        signal: ComparableFloat,
697        /// Bytes transmitted since power on
698        tx: u32,
699        /// Bytes received since power on
700        rx: u32,
701        reserved: i16,
702    },
703
704    /// Gets Host MCU firmware information
705    ///
706    /// Causes the device to transmit a [Message::StateHostFirmware] message.
707    ///
708    /// Message type 14
709    GetHostFirmware,
710
711    /// Response to [Message::GetHostFirmware] message.
712    ///
713    /// Provides host firmware information.
714    ///
715    /// Message type 15
716    StateHostFirmware {
717        /// Firmware build time (absolute time in nanoseconds since epoch)
718        build: u64,
719        reserved: u64,
720        /// The minor component of the firmware version
721        version_minor: u16,
722        /// The major component of the firmware version
723        version_major: u16,
724    },
725
726    /// Get Wifi subsystem information. No payload is required. Causes the device to transmit a
727    /// [Message::StateWifiInfo] message.
728    ///
729    /// Message type 16
730    GetWifiInfo,
731
732    /// StateWifiInfo - 17
733    ///
734    /// Response to [Message::GetWifiInfo] message.
735    ///
736    /// Provides Wifi subsystem information.
737    ///
738    /// Message type 17
739    StateWifiInfo {
740        /// Radio receive signal strength
741        ///
742        /// The units of this field varies between different products.  See this LIFX doc for more info:
743        /// <https://lan.developer.lifx.com/docs/information-messages#statewifiinfo---packet-17>
744        #[cfg(not(fuzzing))]
745        signal: f32,
746        #[cfg(fuzzing)]
747        signal: ComparableFloat,
748        /// Reserved
749        ///
750        /// This field used to store bytes transmitted since power on
751        reserved6: u32,
752        /// Reserved
753        ///
754        /// This field used to store bytes received since power on
755        reserved7: u32,
756        reserved: i16,
757    },
758
759    /// Get Wifi subsystem firmware
760    ///
761    /// Causes the device to transmit a [Message::StateWifiFirmware] message.
762    ///
763    /// Message type 18
764    GetWifiFirmware,
765
766    /// Response to [Message::GetWifiFirmware] message.
767    ///
768    /// Provides Wifi subsystem information.
769    ///
770    /// Message type 19
771    StateWifiFirmware {
772        /// firmware build time (absolute time in nanoseconds since epoch)
773        build: u64,
774        reserved: u64,
775        /// The minor component of the firmware version
776        version_minor: u16,
777        /// The major component of the firmware version
778        version_major: u16,
779    },
780
781    /// Get device power level
782    ///
783    /// Causes the device to transmit a [Message::StatePower] message
784    ///
785    /// Message type 20
786    GetPower,
787
788    /// Set device power level.
789    ///
790    /// Message type 21
791    SetPower {
792        /// normally a u16, but only 0 and 65535 are supported.
793        ///
794        /// Zero implies standby and non-zero sets a corresponding power draw level.
795        level: PowerLevel,
796    },
797
798    /// Response to [Message::GetPower] message.
799    ///
800    /// Provides device power level.
801    ///
802    /// Message type 22
803    StatePower {
804        /// The current level of the device's power
805        ///
806        /// A value of `0` means off, and any other value means on.  Note that `65535`
807        /// is full power and during a power transition the value may be any value
808        /// between `0` and `65535`.
809        level: u16,
810    },
811
812    ///
813    /// Get device label
814    ///
815    /// Causes the device to transmit a [Message::StateLabel] message.
816    ///
817    /// Message type 23
818    GetLabel,
819
820    /// Set the device label text.
821    ///
822    /// Message type 24
823    SetLabel { label: LifxString },
824
825    /// Response to [Message::GetLabel] message.
826    ///
827    /// Provides device label.
828    ///
829    /// Message type 25
830    StateLabel { label: LifxString },
831
832    /// Get the hardware version
833    ///
834    /// Causes the device to transmit a [Message::StateVersion] message.
835    ///
836    /// Message type 32
837    GetVersion,
838
839    /// Response to [Message::GetVersion] message.
840    ///
841    /// Provides the hardware version of the device. To get more information about this product,
842    /// use the [get_product_info] function.
843    ///
844    /// Message type 33
845    StateVersion {
846        /// vendor ID
847        ///
848        /// For LIFX products, this value is `1`.
849        vendor: u32,
850        /// product ID
851        product: u32,
852        /// Reserved
853        ///
854        /// Previously, this field stored the hardware version
855        reserved: u32,
856    },
857
858    /// Get run-time information
859    ///
860    /// Causes the device to transmit a [Message::StateInfo] message.
861    ///
862    /// Message type 34
863    GetInfo,
864
865    /// Response to [Message::GetInfo] message.
866    ///
867    /// Provides run-time information of device.
868    ///
869    /// Message type 35
870    StateInfo {
871        /// The current time according to the device
872        ///
873        /// Note that this is most likely inaccurate.
874        ///
875        /// (absolute time in nanoseconds since epoch)
876        time: u64,
877        /// The amount of time in nanoseconds the device has been online since last power on
878        uptime: u64,
879        /// The amount of time in nanseconds of power off time accurate to 5 seconds.
880        downtime: u64,
881    },
882
883    /// Response to any message sent with ack_required set to 1. See message header frame address.
884    ///
885    /// (Note that technically this message has no payload, but the frame sequence number is stored
886    /// here for convenience).
887    ///
888    /// Message type 45
889    Acknowledgement { seq: u8 },
890
891    /// Ask the bulb to return its location information
892    ///
893    /// Causes the device to transmit a [Message::StateLocation] message.
894    ///
895    /// Message type 48
896    GetLocation,
897
898    /// Set the device location
899    ///
900    /// Message type 49
901    SetLocation {
902        /// GUID byte array
903        location: LifxIdent,
904        /// The name assigned to this location
905        label: LifxString,
906        /// An epoch in nanoseconds of when this location was set on the device
907        updated_at: u64,
908    },
909
910    /// Device location.
911    ///
912    /// Message type 50
913    StateLocation {
914        location: LifxIdent,
915        label: LifxString,
916        updated_at: u64,
917    },
918
919    /// Ask the bulb to return its group membership information
920    ///
921    /// Causes the device to transmit a [Message::StateGroup] message.
922    ///
923    /// Message type 51
924    GetGroup,
925
926    /// Set the device group
927    ///
928    /// Message type 52
929    SetGroup {
930        group: LifxIdent,
931        label: LifxString,
932        updated_at: u64,
933    },
934
935    /// Device group.
936    ///
937    /// Message type 53
938    StateGroup {
939        /// The unique identifier of this group as a `uuid`.
940        group: LifxIdent,
941        /// The name assigned to this group
942        label: LifxString,
943        /// An epoch in nanoseconds of when this group was set on the device
944        updated_at: u64,
945    },
946
947    /// Request an arbitrary payload be echoed back
948    ///
949    /// Causes the device to transmit an [Message::EchoResponse] message.
950    ///
951    /// Message type 58
952    EchoRequest { payload: EchoPayload },
953
954    /// Response to [Message::EchoRequest] message.
955    ///
956    /// Echo response with payload sent in the EchoRequest.
957    ///
958    /// Message type 59
959    EchoResponse { payload: EchoPayload },
960
961    /// Sent by a client to obtain the light state.
962    ///
963    /// Causes the device to transmit a [Message::LightState] message.
964    ///
965    /// Note: this message is also known as `GetColor` in the LIFX docs.  Message type 101
966    LightGet,
967
968    /// Sent by a client to change the light state.
969    ///
970    /// If the Frame Address res_required field is set to one (1) then the device will transmit a
971    /// State message.
972    ///
973    /// Message type 102
974    LightSetColor {
975        reserved: u8,
976        /// Color in HSBK
977        color: HSBK,
978        /// Color transition time in milliseconds
979        duration: u32,
980    },
981
982    /// Apply an effect to the bulb.
983    ///
984    /// Message type 103
985    SetWaveform {
986        reserved: u8,
987        transient: bool,
988        color: HSBK,
989        /// Duration of a cycle in milliseconds
990        period: u32,
991        /// Number of cycles
992        #[cfg(not(fuzzing))]
993        cycles: f32,
994        #[cfg(fuzzing)]
995        cycles: ComparableFloat,
996        /// Waveform Skew, [-32768, 32767] scaled to [0, 1].
997        skew_ratio: i16,
998        /// Waveform to use for transition.
999        waveform: Waveform,
1000    },
1001
1002    /// Sent by a device to provide the current light state.
1003    ///
1004    /// This message is sent in reply to [Message::LightGet], [Message::LightSetColor], [Message::SetWaveform], and [Message::SetWaveformOptional]
1005    ///
1006    /// Message type 107
1007    LightState {
1008        color: HSBK,
1009        reserved: i16,
1010        /// The current power level of the device
1011        power: u16,
1012        /// The current label on the device
1013        label: LifxString,
1014        reserved2: u64,
1015    },
1016
1017    /// Sent by a client to obtain the power level
1018    ///
1019    /// Causes the device to transmit a [Message::LightStatePower] message.
1020    ///
1021    /// Message type 116
1022    LightGetPower,
1023
1024    /// Sent by a client to change the light power level.
1025    ///
1026    /// The duration is the power level transition time in milliseconds.
1027    ///
1028    /// If the Frame Address res_required field is set to one (1) then the device will transmit a
1029    /// StatePower message.
1030    ///
1031    /// Message type 117
1032    LightSetPower { level: u16, duration: u32 },
1033
1034    /// Sent by a device to provide the current power level.
1035    ///
1036    /// Message type 118
1037    LightStatePower { level: u16 },
1038
1039    /// Apply an effect to the bulb.
1040    ///
1041    /// Message type 119
1042    SetWaveformOptional {
1043        reserved: u8,
1044        transient: bool,
1045        color: HSBK,
1046        /// Duration of a cycle in milliseconds
1047        period: u32,
1048        /// Number of cycles
1049        #[cfg(not(fuzzing))]
1050        cycles: f32,
1051        #[cfg(fuzzing)]
1052        cycles: ComparableFloat,
1053
1054        skew_ratio: i16,
1055        waveform: Waveform,
1056        set_hue: bool,
1057        set_saturation: bool,
1058        set_brightness: bool,
1059        set_kelvin: bool,
1060    },
1061
1062    /// Gets the current maximum power level of the Infrared channel
1063    ///
1064    /// Message type 120
1065    LightGetInfrared,
1066
1067    /// Indicates the current maximum setting for the infrared channel.
1068    ///
1069    /// Message type 121
1070    LightStateInfrared { brightness: u16 },
1071
1072    /// Set the current maximum brightness for the infrared channel.
1073    ///
1074    /// Message type 122
1075    LightSetInfrared { brightness: u16 },
1076
1077    /// Get the state of the HEV LEDs on the device
1078    ///
1079    /// Causes the device to transmite a [Messages::LightStateHevCycle] message.
1080    ///
1081    /// This message requires the device has the `hev` capability
1082    ///
1083    /// Message type 142
1084    LightGetHevCycle,
1085
1086    /// Message type 143
1087    LightSetHevCycle {
1088        /// Set this to false to turn off the cycle and true to start the cycle
1089        enable: bool,
1090        /// The duration, in seconds that the cycle should last for
1091        ///
1092        /// A value of 0 will use the default duration set by SetHevCycleConfiguration (146).
1093        duration: u32,
1094    },
1095
1096    /// Whether a HEV cycle is running on the device
1097    ///
1098    /// Message type 144
1099    LightStateHevCycle {
1100        /// The duration, in seconds, this cycle was set to
1101        duration: u32,
1102        /// The duration, in seconds, remaining in this cycle
1103        remaining: u32,
1104        /// The power state before the HEV cycle started, which will be the power state once the cycle completes.
1105        ///
1106        /// This is only relevant if `remaining` is larger than 0.
1107        last_power: bool,
1108    },
1109
1110    /// Getthe default configuration for using the HEV LEDs on the device
1111    ///
1112    /// This message requires the device has the `hev` capability
1113    ///
1114    /// Message type 145
1115    LightGetHevCycleConfiguration,
1116
1117    /// Message type 146
1118    LightSetHevCycleConfiguration { indication: bool, duration: u32 },
1119
1120    /// Message type 147
1121    LightStateHevCycleConfiguration { indication: bool, duration: u32 },
1122
1123    /// Message type 148
1124    LightGetLastHevCycleResult,
1125
1126    /// Message type 149
1127    LightStateLastHevCycleResult { result: LastHevCycleResult },
1128
1129    /// This message is used for changing the color of either a single or multiple zones.
1130    /// The changes are stored in a buffer and are only applied once a message with either
1131    /// [ApplicationRequest::Apply] or [ApplicationRequest::ApplyOnly] set.
1132    ///
1133    /// Message type 501
1134    SetColorZones {
1135        start_index: u8,
1136        end_index: u8,
1137        color: HSBK,
1138        duration: u32,
1139        apply: ApplicationRequest,
1140    },
1141
1142    /// GetColorZones is used to request the zone colors for a range of zones.
1143    ///
1144    /// The bulb will respond
1145    /// with either [Message::StateZone] or [Message::StateMultiZone] messages as required to cover
1146    /// the requested range. The bulb may send state messages that cover more than the requested
1147    /// zones. Any zones outside the requested indexes will still contain valid values at the time
1148    /// the message was sent.
1149    ///
1150    /// Message type 502
1151    GetColorZones { start_index: u8, end_index: u8 },
1152
1153    /// The StateZone message represents the state of a single zone with the `index` field indicating
1154    /// which zone is represented. The `count` field contains the count of the total number of zones
1155    /// available on the device.
1156    ///
1157    /// Message type 503
1158    StateZone { count: u8, index: u8, color: HSBK },
1159
1160    /// The StateMultiZone message represents the state of eight consecutive zones in a single message.
1161    /// As in the StateZone message the `count` field represents the count of the total number of
1162    /// zones available on the device. In this message the `index` field represents the index of
1163    /// `color0` and the rest of the colors are the consecutive zones thus the index of the
1164    /// `color_n` zone will be `index + n`.
1165    ///
1166    /// Message type 506
1167    StateMultiZone {
1168        count: u8,
1169        index: u8,
1170        color0: HSBK,
1171        color1: HSBK,
1172        color2: HSBK,
1173        color3: HSBK,
1174        color4: HSBK,
1175        color5: HSBK,
1176        color6: HSBK,
1177        color7: HSBK,
1178    },
1179
1180    /// Message type 507
1181    GetMultiZoneEffect,
1182
1183    /// Message type 508
1184    SetMultiZoneEffect {
1185        /// The unique value identifying this effect
1186        instance_id: u32,
1187        typ: MultiZoneEffectType,
1188        reserved: u16,
1189        /// The time it takes for one cycle of the effect in milliseconds
1190        speed: u32,
1191        /// The amount of time left in the current effect in nanoseconds
1192        duration: u64,
1193        reserved7: u32,
1194        reserved8: u32,
1195        /// The parameters that was used in the request.
1196        parameters: [u32; 8],
1197    },
1198
1199    /// Message type 509
1200    StateMultiZoneEffect {
1201        /// The unique value identifying this effect
1202        instance_id: u32,
1203        typ: MultiZoneEffectType,
1204        reserved: u16,
1205        /// The time it takes for one cycle of the effect in milliseconds
1206        speed: u32,
1207        /// The amount of time left in the current effect in nanoseconds
1208        duration: u64,
1209        reserved7: u32,
1210        reserved8: u32,
1211        /// The parameters that was used in the request.
1212        parameters: [u32; 8],
1213    },
1214
1215    /// Message type 510
1216    SetExtendedColorZones {
1217        duration: u32,
1218        apply: ApplicationRequest,
1219        zone_index: u16,
1220        colors_count: u8,
1221        colors: Box<[HSBK; 82]>,
1222    },
1223
1224    /// Message type 511
1225    GetExtendedColorZone,
1226
1227    /// Message type 512
1228    StateExtendedColorZones {
1229        zones_count: u16,
1230        zone_index: u16,
1231        colors_count: u8,
1232        colors: Box<[HSBK; 82]>,
1233    },
1234
1235    /// Get the power state of a relay
1236    ///
1237    /// This requires the device has the `relays` capability.
1238    ///
1239    /// Message type 816
1240    RelayGetPower {
1241        /// The relay on the switch starting from 0
1242        relay_index: u8,
1243    },
1244
1245    /// Message ty 817
1246    RelaySetPower {
1247        /// The relay on the switch starting from 0
1248        relay_index: u8,
1249        /// The value of the relay
1250        ///
1251        /// Current models of the LIFX switch do not have dimming capability, so the two valid values are `0`
1252        /// for off and `65535` for on.
1253        level: u16,
1254    },
1255
1256    /// The state of the device relay
1257    ///
1258    /// Message type 818
1259    RelayStatePower {
1260        /// The relay on the switch starting from 0
1261        relay_index: u8,
1262        /// The value of the relay
1263        ///
1264        /// Current models of the LIFX switch do not have dimming capability, so the two valid values are `0`
1265        /// for off and `65535` for on.
1266        level: u16,
1267    },
1268}
1269
1270impl Message {
1271    /// Get the message type
1272    ///
1273    /// This will be used in the `typ` field of the [ProtocolHeader].
1274    pub fn get_num(&self) -> u16 {
1275        match *self {
1276            Message::GetService => 2,
1277            Message::StateService { .. } => 3,
1278            Message::GetHostInfo => 12,
1279            Message::StateHostInfo { .. } => 13,
1280            Message::GetHostFirmware => 14,
1281            Message::StateHostFirmware { .. } => 15,
1282            Message::GetWifiInfo => 16,
1283            Message::StateWifiInfo { .. } => 17,
1284            Message::GetWifiFirmware => 18,
1285            Message::StateWifiFirmware { .. } => 19,
1286            Message::GetPower => 20,
1287            Message::SetPower { .. } => 21,
1288            Message::StatePower { .. } => 22,
1289            Message::GetLabel => 23,
1290            Message::SetLabel { .. } => 24,
1291            Message::StateLabel { .. } => 25,
1292            Message::GetVersion => 32,
1293            Message::StateVersion { .. } => 33,
1294            Message::GetInfo => 34,
1295            Message::StateInfo { .. } => 35,
1296            Message::Acknowledgement { .. } => 45,
1297            Message::GetLocation => 48,
1298            Message::SetLocation { .. } => 49,
1299            Message::StateLocation { .. } => 50,
1300            Message::GetGroup => 51,
1301            Message::SetGroup { .. } => 52,
1302            Message::StateGroup { .. } => 53,
1303            Message::EchoRequest { .. } => 58,
1304            Message::EchoResponse { .. } => 59,
1305            Message::LightGet => 101,
1306            Message::LightSetColor { .. } => 102,
1307            Message::SetWaveform { .. } => 103,
1308            Message::LightState { .. } => 107,
1309            Message::LightGetPower => 116,
1310            Message::LightSetPower { .. } => 117,
1311            Message::LightStatePower { .. } => 118,
1312            Message::SetWaveformOptional { .. } => 119,
1313            Message::LightGetInfrared => 120,
1314            Message::LightStateInfrared { .. } => 121,
1315            Message::LightSetInfrared { .. } => 122,
1316            Message::LightGetHevCycle => 142,
1317            Message::LightSetHevCycle { .. } => 143,
1318            Message::LightStateHevCycle { .. } => 144,
1319            Message::LightGetHevCycleConfiguration => 145,
1320            Message::LightSetHevCycleConfiguration { .. } => 146,
1321            Message::LightStateHevCycleConfiguration { .. } => 147,
1322            Message::LightGetLastHevCycleResult => 148,
1323            Message::LightStateLastHevCycleResult { .. } => 149,
1324            Message::SetColorZones { .. } => 501,
1325            Message::GetColorZones { .. } => 502,
1326            Message::StateZone { .. } => 503,
1327            Message::StateMultiZone { .. } => 506,
1328            Message::GetMultiZoneEffect => 507,
1329            Message::SetMultiZoneEffect { .. } => 508,
1330            Message::StateMultiZoneEffect { .. } => 509,
1331            Message::SetExtendedColorZones { .. } => 510,
1332            Message::GetExtendedColorZone => 511,
1333            Message::StateExtendedColorZones { .. } => 512,
1334            Message::RelayGetPower { .. } => 816,
1335            Message::RelaySetPower { .. } => 817,
1336            Message::RelayStatePower { .. } => 818,
1337        }
1338    }
1339
1340    /// Tries to parse the payload in a [RawMessage], based on its message type.
1341    pub fn from_raw(msg: &RawMessage) -> Result<Message, Error> {
1342        match msg.protocol_header.typ {
1343            2 => Ok(Message::GetService),
1344            3 => Ok(unpack!(msg, StateService, service: u8, port: u32)),
1345            12 => Ok(Message::GetHostInfo),
1346            13 => Ok(unpack!(
1347                msg,
1348                StateHostInfo,
1349                signal: f32,
1350                tx: u32,
1351                rx: u32,
1352                reserved: i16
1353            )),
1354            14 => Ok(Message::GetHostFirmware),
1355            15 => Ok(unpack!(
1356                msg,
1357                StateHostFirmware,
1358                build: u64,
1359                reserved: u64,
1360                version_minor: u16,
1361                version_major: u16
1362            )),
1363            16 => Ok(Message::GetWifiInfo),
1364            17 => Ok(unpack!(
1365                msg,
1366                StateWifiInfo,
1367                signal: f32,
1368                reserved6: u32,
1369                reserved7: u32,
1370                reserved: i16
1371            )),
1372            18 => Ok(Message::GetWifiFirmware),
1373            19 => Ok(unpack!(
1374                msg,
1375                StateWifiFirmware,
1376                build: u64,
1377                reserved: u64,
1378                version_minor: u16,
1379                version_major: u16
1380            )),
1381            20 => Ok(Message::GetPower),
1382            21 => Ok(unpack!(msg, SetPower, level: PowerLevel)),
1383            22 => Ok(unpack!(msg, StatePower, level: u16)),
1384            23 => Ok(Message::GetLabel),
1385            24 => Ok(unpack!(msg, SetLabel, label: LifxString)),
1386            25 => Ok(unpack!(msg, StateLabel, label: LifxString)),
1387            32 => Ok(Message::GetVersion),
1388            33 => Ok(unpack!(
1389                msg,
1390                StateVersion,
1391                vendor: u32,
1392                product: u32,
1393                reserved: u32
1394            )),
1395            34 => Ok(Message::GetInfo),
1396            35 => Ok(unpack!(
1397                msg,
1398                StateInfo,
1399                time: u64,
1400                uptime: u64,
1401                downtime: u64
1402            )),
1403            45 => Ok(Message::Acknowledgement {
1404                seq: msg.frame_addr.sequence,
1405            }),
1406            48 => Ok(Message::GetLocation),
1407            49 => Ok(unpack!(
1408                msg,
1409                SetLocation,
1410                location: LifxIdent,
1411                label: LifxString,
1412                updated_at: u64
1413            )),
1414            50 => Ok(unpack!(
1415                msg,
1416                StateLocation,
1417                location: LifxIdent,
1418                label: LifxString,
1419                updated_at: u64
1420            )),
1421            51 => Ok(Message::GetGroup),
1422            52 => Ok(unpack!(
1423                msg,
1424                SetGroup,
1425                group: LifxIdent,
1426                label: LifxString,
1427                updated_at: u64
1428            )),
1429            53 => Ok(unpack!(
1430                msg,
1431                StateGroup,
1432                group: LifxIdent,
1433                label: LifxString,
1434                updated_at: u64
1435            )),
1436            58 => Ok(unpack!(msg, EchoRequest, payload: EchoPayload)),
1437            59 => Ok(unpack!(msg, EchoResponse, payload: EchoPayload)),
1438            101 => Ok(Message::LightGet),
1439            102 => Ok(unpack!(
1440                msg,
1441                LightSetColor,
1442                reserved: u8,
1443                color: HSBK,
1444                duration: u32
1445            )),
1446            103 => Ok(unpack!(
1447                msg,
1448                SetWaveform,
1449                reserved: u8,
1450                transient: bool,
1451                color: HSBK,
1452                period: u32,
1453                cycles: f32,
1454                skew_ratio: i16,
1455                waveform: Waveform
1456            )),
1457            107 => Ok(unpack!(
1458                msg,
1459                LightState,
1460                color: HSBK,
1461                reserved: i16,
1462                power: u16,
1463                label: LifxString,
1464                reserved2: u64
1465            )),
1466            116 => Ok(Message::LightGetPower),
1467            117 => Ok(unpack!(msg, LightSetPower, level: u16, duration: u32)),
1468            118 => {
1469                let mut c = Cursor::new(&msg.payload);
1470                Ok(Message::LightStatePower {
1471                    level: c.read_val()?,
1472                })
1473            }
1474            119 => Ok(unpack!(
1475                msg,
1476                SetWaveformOptional,
1477                reserved: u8,
1478                transient: bool,
1479                color: HSBK,
1480                period: u32,
1481                cycles: f32,
1482                skew_ratio: i16,
1483                waveform: Waveform,
1484                set_hue: bool,
1485                set_saturation: bool,
1486                set_brightness: bool,
1487                set_kelvin: bool
1488            )),
1489            120 => Ok(Message::LightGetInfrared),
1490            122 => Ok(unpack!(msg, LightSetInfrared, brightness: u16)),
1491            142 => Ok(Message::LightGetHevCycle),
1492            143 => Ok(unpack!(msg, LightSetHevCycle, enable: bool, duration: u32)),
1493            144 => Ok(unpack!(
1494                msg,
1495                LightStateHevCycle,
1496                duration: u32,
1497                remaining: u32,
1498                last_power: bool
1499            )),
1500            145 => Ok(Message::LightGetHevCycleConfiguration),
1501            146 => Ok(unpack!(
1502                msg,
1503                LightSetHevCycleConfiguration,
1504                indication: bool,
1505                duration: u32
1506            )),
1507            147 => Ok(unpack!(
1508                msg,
1509                LightStateHevCycleConfiguration,
1510                indication: bool,
1511                duration: u32
1512            )),
1513            148 => Ok(Message::LightGetLastHevCycleResult),
1514            149 => Ok(unpack!(
1515                msg,
1516                LightStateLastHevCycleResult,
1517                result: LastHevCycleResult
1518            )),
1519            121 => Ok(unpack!(msg, LightStateInfrared, brightness: u16)),
1520            501 => Ok(unpack!(
1521                msg,
1522                SetColorZones,
1523                start_index: u8,
1524                end_index: u8,
1525                color: HSBK,
1526                duration: u32,
1527                apply: u8
1528            )),
1529            502 => Ok(unpack!(msg, GetColorZones, start_index: u8, end_index: u8)),
1530            503 => Ok(unpack!(msg, StateZone, count: u8, index: u8, color: HSBK)),
1531            506 => Ok(unpack!(
1532                msg,
1533                StateMultiZone,
1534                count: u8,
1535                index: u8,
1536                color0: HSBK,
1537                color1: HSBK,
1538                color2: HSBK,
1539                color3: HSBK,
1540                color4: HSBK,
1541                color5: HSBK,
1542                color6: HSBK,
1543                color7: HSBK
1544            )),
1545            507 => Ok(Message::GetMultiZoneEffect),
1546            508 => Ok(unpack!(
1547                msg,
1548                SetMultiZoneEffect,
1549                instance_id: u32,
1550                typ: MultiZoneEffectType,
1551                reserved: u16,
1552                speed: u32,
1553                duration: u64,
1554                reserved7: u32,
1555                reserved8: u32,
1556                parameters: [u32; 8]
1557            )),
1558            509 => Ok(unpack!(
1559                msg,
1560                StateMultiZoneEffect,
1561                instance_id: u32,
1562                typ: MultiZoneEffectType,
1563                reserved: u16,
1564                speed: u32,
1565                duration: u64,
1566                reserved7: u32,
1567                reserved8: u32,
1568                parameters: [u32; 8]
1569            )),
1570            510 => Ok(unpack!(
1571                msg,
1572                SetExtendedColorZones,
1573                duration: u32,
1574                apply: u8,
1575                zone_index: u16,
1576                colors_count: u8,
1577                colors: [HSBK; 82]
1578            )),
1579            511 => Ok(Message::GetExtendedColorZone),
1580            512 => Ok(unpack!(
1581                msg,
1582                StateExtendedColorZones,
1583                zones_count: u16,
1584                zone_index: u16,
1585                colors_count: u8,
1586                colors: [HSBK; 82]
1587            )),
1588            816 => Ok(unpack!(msg, RelayGetPower, relay_index: u8)),
1589            817 => Ok(unpack!(msg, RelaySetPower, relay_index: u8, level: u16)),
1590            818 => Ok(unpack!(msg, RelayStatePower, relay_index: u8, level: u16)),
1591            _ => Err(Error::UnknownMessageType(msg.protocol_header.typ)),
1592        }
1593    }
1594}
1595
1596/// Bulb color (Hue-Saturation-Brightness-Kelvin)
1597///
1598/// # Notes:
1599///
1600/// Colors are represented as Hue-Saturation-Brightness-Kelvin, or HSBK
1601///
1602/// When a light is displaying whites, saturation will be zero, hue will be ignored, and only
1603/// brightness and kelvin will matter.
1604///
1605/// Normal values for "kelvin" are from 2500 (warm/yellow) to 9000 (cool/blue)
1606///
1607/// When a light is displaying colors, kelvin is ignored.
1608///
1609/// To display "pure" colors, set saturation to full (65535).
1610#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1611#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1612pub struct HSBK {
1613    pub hue: u16,
1614    pub saturation: u16,
1615    pub brightness: u16,
1616    pub kelvin: u16,
1617}
1618
1619impl HSBK {
1620    pub fn describe(&self, short: bool) -> String {
1621        match short {
1622            true if self.saturation == 0 => format!("{}K", self.kelvin),
1623            true => format!(
1624                "{:.0}/{:.0}",
1625                (self.hue as f32 / 65535.0) * 360.0,
1626                self.saturation as f32 / 655.35
1627            ),
1628            false if self.saturation == 0 => format!(
1629                "{:.0}% White ({})",
1630                self.brightness as f32 / 655.35,
1631                describe_kelvin(self.kelvin)
1632            ),
1633            false => format!(
1634                "{}% hue: {} sat: {}",
1635                self.brightness as f32 / 655.35,
1636                self.hue,
1637                self.saturation
1638            ),
1639        }
1640    }
1641}
1642
1643/// Describe (in english words) the color temperature as given in kelvin.
1644///
1645/// These descriptions match the values shown in the LIFX mobile app.
1646pub fn describe_kelvin(k: u16) -> &'static str {
1647    if k <= 2500 {
1648        "Ultra Warm"
1649    } else if k > 2500 && k <= 2700 {
1650        "Incandescent"
1651    } else if k > 2700 && k <= 3000 {
1652        "Warm"
1653    } else if k > 300 && k <= 3200 {
1654        "Neutral Warm"
1655    } else if k > 3200 && k <= 3500 {
1656        "Neutral"
1657    } else if k > 3500 && k <= 4000 {
1658        "Cool"
1659    } else if k > 400 && k <= 4500 {
1660        "Cool Daylight"
1661    } else if k > 4500 && k <= 5000 {
1662        "Soft Daylight"
1663    } else if k > 5000 && k <= 5500 {
1664        "Daylight"
1665    } else if k > 5500 && k <= 6000 {
1666        "Noon Daylight"
1667    } else if k > 6000 && k <= 6500 {
1668        "Bright Daylight"
1669    } else if k > 6500 && k <= 7000 {
1670        "Cloudy Daylight"
1671    } else if k > 7000 && k <= 7500 {
1672        "Blue Daylight"
1673    } else if k > 7500 && k <= 8000 {
1674        "Blue Overcast"
1675    } else if k > 8000 && k <= 8500 {
1676        "Blue Water"
1677    } else {
1678        "Blue Ice"
1679    }
1680}
1681
1682impl HSBK {}
1683
1684/// The raw message structure
1685///
1686/// Contains a low-level protocol info.  This is what is sent and received via UDP packets.
1687///
1688/// To parse the payload, use [Message::from_raw].
1689#[derive(Debug, Clone, PartialEq, Eq)]
1690pub struct RawMessage {
1691    pub frame: Frame,
1692    pub frame_addr: FrameAddress,
1693    pub protocol_header: ProtocolHeader,
1694    pub payload: Vec<u8>,
1695}
1696
1697/// The Frame section contains information about the following:
1698///
1699/// * Size of the entire message
1700/// * LIFX Protocol number: must be 1024 (decimal)
1701/// * Use of the Frame Address target field
1702/// * Source identifier
1703///
1704/// The `tagged` field is a boolean that indicates whether the Frame Address target field is
1705/// being used to address an individual device or all devices.  If `tagged` is true, then the
1706/// `target` field should be all zeros.
1707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1708pub struct Frame {
1709    /// 16 bits: Size of entire message in bytes including this field
1710    pub size: u16,
1711
1712    /// 2 bits: Message origin indicator: must be zero (0)
1713    pub origin: u8,
1714
1715    /// 1 bit: Determines usage of the Frame Address target field
1716    pub tagged: bool,
1717
1718    /// 1 bit: Message includes a target address: must be one (1)
1719    pub addressable: bool,
1720
1721    /// 12 bits: Protocol number: must be 1024 (decimal)
1722    pub protocol: u16,
1723
1724    /// 32 bits: Source identifier: unique value set by the client, used by responses.
1725    ///
1726    /// If the source identifier is zero, then the LIFX device may send a broadcast message that can
1727    /// be received by all clients on the same subnet.
1728    ///
1729    /// If this packet is a reply, then this source field will be set to the same value as the client-
1730    /// sent request packet.
1731    pub source: u32,
1732}
1733
1734/// The Frame Address section contains the following routing information:
1735///
1736/// * Target device address
1737/// * Acknowledgement message is required flag
1738/// * State response message is required flag
1739/// * Message sequence number
1740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1741pub struct FrameAddress {
1742    /// 64 bits: 6 byte device address (MAC address) or zero (0) means all devices
1743    pub target: u64,
1744
1745    /// 48 bits: Must all be zero (0)
1746    pub reserved: [u8; 6],
1747
1748    /// 6 bits: Reserved
1749    pub reserved2: u8,
1750
1751    /// 1 bit: Acknowledgement message required
1752    pub ack_required: bool,
1753
1754    /// 1 bit: Response message required
1755    pub res_required: bool,
1756
1757    /// 8 bits: Wrap around message sequence number
1758    pub sequence: u8,
1759}
1760
1761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1762pub struct ProtocolHeader {
1763    /// 64 bits: Reserved
1764    pub reserved: u64,
1765
1766    /// 16 bits: Message type determines the payload being used
1767    ///
1768    /// See also [Message::get_num]
1769    pub typ: u16,
1770
1771    /// 16 bits: Reserved
1772    pub reserved2: u16,
1773}
1774
1775impl Frame {
1776    /// packed sized, in bytes
1777    fn packed_size() -> usize {
1778        8
1779    }
1780
1781    fn validate(&self) {
1782        assert!(self.origin < 4);
1783        assert!(self.addressable);
1784        assert_eq!(self.protocol, 1024);
1785    }
1786
1787    fn pack(&self) -> Result<Vec<u8>, Error> {
1788        let mut v = Vec::with_capacity(Self::packed_size());
1789
1790        v.write_u16::<LittleEndian>(self.size)?;
1791
1792        // pack origin + tagged + addressable +  protocol as a u16
1793        let mut d: u16 = (<u16 as From<u8>>::from(self.origin) & 0b11) << 14;
1794        d += if self.tagged { 1 } else { 0 } << 13;
1795        d += if self.addressable { 1 } else { 0 } << 12;
1796        d += (self.protocol & 0b1111_1111_1111) as u16;
1797
1798        v.write_u16::<LittleEndian>(d)?;
1799
1800        v.write_u32::<LittleEndian>(self.source)?;
1801
1802        Ok(v)
1803    }
1804
1805    fn unpack(v: &[u8]) -> Result<Frame, Error> {
1806        let mut c = Cursor::new(v);
1807
1808        let size = c.read_val()?;
1809
1810        // origin + tagged + addressable + protocol
1811        let d: u16 = c.read_val()?;
1812
1813        let origin: u8 = ((d & 0b1100_0000_0000_0000) >> 14) as u8;
1814        let tagged: bool = (d & 0b0010_0000_0000_0000) > 0;
1815        let addressable = (d & 0b0001_0000_0000_0000) > 0;
1816        let protocol: u16 = d & 0b0000_1111_1111_1111;
1817
1818        if protocol != 1024 {
1819            return Err(Error::ProtocolError(format!(
1820                "Unpacked frame had protocol version {}",
1821                protocol
1822            )));
1823        }
1824
1825        let source = c.read_val()?;
1826
1827        let frame = Frame {
1828            size,
1829            origin,
1830            tagged,
1831            addressable,
1832            protocol,
1833            source,
1834        };
1835        Ok(frame)
1836    }
1837}
1838
1839impl FrameAddress {
1840    fn packed_size() -> usize {
1841        16
1842    }
1843    fn validate(&self) {
1844        //assert_eq!(self.reserved, [0;6]);
1845        //assert_eq!(self.reserved2, 0);
1846    }
1847    fn pack(&self) -> Result<Vec<u8>, Error> {
1848        let mut v = Vec::with_capacity(Self::packed_size());
1849        v.write_u64::<LittleEndian>(self.target)?;
1850        for idx in 0..6 {
1851            v.write_u8(self.reserved[idx])?;
1852        }
1853
1854        let b: u8 = (self.reserved2 << 2)
1855            + if self.ack_required { 2 } else { 0 }
1856            + if self.res_required { 1 } else { 0 };
1857        v.write_u8(b)?;
1858        v.write_u8(self.sequence)?;
1859        Ok(v)
1860    }
1861
1862    fn unpack(v: &[u8]) -> Result<FrameAddress, Error> {
1863        let mut c = Cursor::new(v);
1864
1865        let target = c.read_val()?;
1866
1867        let mut reserved: [u8; 6] = [0; 6];
1868        for slot in &mut reserved {
1869            *slot = c.read_val()?;
1870        }
1871
1872        let b: u8 = c.read_val()?;
1873        let reserved2: u8 = (b & 0b1111_1100) >> 2;
1874        let ack_required = (b & 0b10) > 0;
1875        let res_required = (b & 0b01) > 0;
1876
1877        let sequence = c.read_val()?;
1878
1879        let f = FrameAddress {
1880            target,
1881            reserved,
1882            reserved2,
1883            ack_required,
1884            res_required,
1885            sequence,
1886        };
1887        f.validate();
1888        Ok(f)
1889    }
1890}
1891
1892impl ProtocolHeader {
1893    fn packed_size() -> usize {
1894        12
1895    }
1896    fn validate(&self) {
1897        //assert_eq!(self.reserved, 0);
1898        //assert_eq!(self.reserved2, 0);
1899    }
1900
1901    /// Packs this part of the packet into some bytes
1902    pub fn pack(&self) -> Result<Vec<u8>, Error> {
1903        let mut v = Vec::with_capacity(Self::packed_size());
1904        v.write_u64::<LittleEndian>(self.reserved)?;
1905        v.write_u16::<LittleEndian>(self.typ)?;
1906        v.write_u16::<LittleEndian>(self.reserved2)?;
1907        Ok(v)
1908    }
1909    fn unpack(v: &[u8]) -> Result<ProtocolHeader, Error> {
1910        let mut c = Cursor::new(v);
1911
1912        let reserved = c.read_val()?;
1913        let typ = c.read_val()?;
1914        let reserved2 = c.read_val()?;
1915
1916        let f = ProtocolHeader {
1917            reserved,
1918            typ,
1919            reserved2,
1920        };
1921        f.validate();
1922        Ok(f)
1923    }
1924}
1925
1926/// Options used to construct a [RawMessage].
1927///
1928/// See also [RawMessage::build].
1929#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1930pub struct BuildOptions {
1931    /// If not `None`, this is the ID of the device you want to address.
1932    ///
1933    /// To look up the ID of a device, extract it from the [FrameAddress::target] field when a
1934    /// device sends a [Message::StateService] message.
1935    pub target: Option<u64>,
1936    /// Acknowledgement message required.
1937    ///
1938    /// Causes the light to send an [Message::Acknowledgement] message.
1939    pub ack_required: bool,
1940    /// Response message required.
1941    ///
1942    /// Some message types are sent by clients to get data from a light.  These should always have
1943    /// `res_required` set to true.
1944    pub res_required: bool,
1945    /// A wrap around sequence number.  Optional (can be zero).
1946    ///
1947    /// By providing a unique sequence value, the response message will also contain the same
1948    /// sequence number, allowing a client to distinguish between different messages sent with the
1949    /// same `source` identifier.
1950    pub sequence: u8,
1951    /// A unique client identifier. Optional (can be zero).
1952    ///
1953    /// If the source is non-zero, then the LIFX device with send a unicast message to the IP
1954    /// address/port of the client that sent the originating message.  If zero, then the LIFX
1955    /// device may send a broadcast message that can be received by all clients on the same sub-net.
1956    pub source: u32,
1957}
1958
1959impl RawMessage {
1960    /// Build a RawMessage (which is suitable for sending on the network) from a given Message
1961    /// type.
1962    ///
1963    /// If [BuildOptions::target] is None, then the message is addressed to all devices.  Else it should be a
1964    /// bulb UID (MAC address)
1965    pub fn build(options: &BuildOptions, typ: Message) -> Result<RawMessage, Error> {
1966        let frame = Frame {
1967            size: 0,
1968            origin: 0,
1969            tagged: options.target.is_none(),
1970            addressable: true,
1971            protocol: 1024,
1972            source: options.source,
1973        };
1974        let addr = FrameAddress {
1975            target: options.target.unwrap_or(0),
1976            reserved: [0; 6],
1977            reserved2: 0,
1978            ack_required: options.ack_required,
1979            res_required: options.res_required,
1980            sequence: options.sequence,
1981        };
1982        let phead = ProtocolHeader {
1983            reserved: 0,
1984            reserved2: 0,
1985            typ: typ.get_num(),
1986        };
1987
1988        let mut v = Vec::new();
1989        match typ {
1990            Message::GetService
1991            | Message::GetHostInfo
1992            | Message::GetHostFirmware
1993            | Message::GetWifiFirmware
1994            | Message::GetWifiInfo
1995            | Message::GetPower
1996            | Message::GetLabel
1997            | Message::GetVersion
1998            | Message::GetInfo
1999            | Message::Acknowledgement { .. }
2000            | Message::GetLocation
2001            | Message::GetGroup
2002            | Message::LightGet
2003            | Message::LightGetPower
2004            | Message::LightGetInfrared
2005            | Message::LightGetHevCycle
2006            | Message::LightGetHevCycleConfiguration
2007            | Message::LightGetLastHevCycleResult
2008            | Message::GetMultiZoneEffect
2009            | Message::GetExtendedColorZone => {
2010                // these types have no payload
2011            }
2012            Message::SetColorZones {
2013                start_index,
2014                end_index,
2015                color,
2016                duration,
2017                apply,
2018            } => {
2019                v.write_val(start_index)?;
2020                v.write_val(end_index)?;
2021                v.write_val(color)?;
2022                v.write_val(duration)?;
2023                v.write_val(apply)?;
2024            }
2025            Message::SetWaveform {
2026                reserved,
2027                transient,
2028                color,
2029                period,
2030                cycles,
2031                skew_ratio,
2032                waveform,
2033            } => {
2034                v.write_val(reserved)?;
2035                v.write_val(transient)?;
2036                v.write_val(color)?;
2037                v.write_val(period)?;
2038                v.write_val(cycles)?;
2039                v.write_val(skew_ratio)?;
2040                v.write_val(waveform)?;
2041            }
2042            Message::SetWaveformOptional {
2043                reserved,
2044                transient,
2045                color,
2046                period,
2047                cycles,
2048                skew_ratio,
2049                waveform,
2050                set_hue,
2051                set_saturation,
2052                set_brightness,
2053                set_kelvin,
2054            } => {
2055                v.write_val(reserved)?;
2056                v.write_val(transient)?;
2057                v.write_val(color)?;
2058                v.write_val(period)?;
2059                v.write_val(cycles)?;
2060                v.write_val(skew_ratio)?;
2061                v.write_val(waveform)?;
2062                v.write_val(set_hue)?;
2063                v.write_val(set_saturation)?;
2064                v.write_val(set_brightness)?;
2065                v.write_val(set_kelvin)?;
2066            }
2067            Message::GetColorZones {
2068                start_index,
2069                end_index,
2070            } => {
2071                v.write_val(start_index)?;
2072                v.write_val(end_index)?;
2073            }
2074            Message::StateZone {
2075                count,
2076                index,
2077                color,
2078            } => {
2079                v.write_val(count)?;
2080                v.write_val(index)?;
2081                v.write_val(color)?;
2082            }
2083            Message::StateMultiZone {
2084                count,
2085                index,
2086                color0,
2087                color1,
2088                color2,
2089                color3,
2090                color4,
2091                color5,
2092                color6,
2093                color7,
2094            } => {
2095                v.write_val(count)?;
2096                v.write_val(index)?;
2097                v.write_val(color0)?;
2098                v.write_val(color1)?;
2099                v.write_val(color2)?;
2100                v.write_val(color3)?;
2101                v.write_val(color4)?;
2102                v.write_val(color5)?;
2103                v.write_val(color6)?;
2104                v.write_val(color7)?;
2105            }
2106            Message::LightStateInfrared { brightness } => v.write_val(brightness)?,
2107            Message::LightSetInfrared { brightness } => v.write_val(brightness)?,
2108            Message::SetLocation {
2109                location,
2110                label,
2111                updated_at,
2112            } => {
2113                v.write_val(location)?;
2114                v.write_val(label)?;
2115                v.write_val(updated_at)?;
2116            }
2117            Message::SetGroup {
2118                group,
2119                label,
2120                updated_at,
2121            } => {
2122                v.write_val(group)?;
2123                v.write_val(label)?;
2124                v.write_val(updated_at)?;
2125            }
2126            Message::StateService { port, service } => {
2127                v.write_val(service as u8)?;
2128                v.write_val(port)?;
2129            }
2130            Message::StateHostInfo {
2131                signal,
2132                tx,
2133                rx,
2134                reserved,
2135            } => {
2136                v.write_val(signal)?;
2137                v.write_val(tx)?;
2138                v.write_val(rx)?;
2139                v.write_val(reserved)?;
2140            }
2141            Message::StateHostFirmware {
2142                build,
2143                reserved,
2144                version_minor,
2145                version_major,
2146            } => {
2147                v.write_val(build)?;
2148                v.write_val(reserved)?;
2149                v.write_val(version_minor)?;
2150                v.write_val(version_major)?;
2151            }
2152            Message::StateWifiInfo {
2153                signal,
2154                reserved6,
2155                reserved7,
2156                reserved,
2157            } => {
2158                v.write_val(signal)?;
2159                v.write_val(reserved6)?;
2160                v.write_val(reserved7)?;
2161                v.write_val(reserved)?;
2162            }
2163            Message::StateWifiFirmware {
2164                build,
2165                reserved,
2166                version_minor,
2167                version_major,
2168            } => {
2169                v.write_val(build)?;
2170                v.write_val(reserved)?;
2171                v.write_val(version_minor)?;
2172                v.write_val(version_major)?;
2173            }
2174            Message::SetPower { level } => {
2175                v.write_val(level)?;
2176            }
2177            Message::StatePower { level } => {
2178                v.write_val(level)?;
2179            }
2180            Message::SetLabel { label } => {
2181                v.write_val(label)?;
2182            }
2183            Message::StateLabel { label } => {
2184                v.write_val(label)?;
2185            }
2186            Message::StateVersion {
2187                vendor,
2188                product,
2189                reserved,
2190            } => {
2191                v.write_val(vendor)?;
2192                v.write_val(product)?;
2193                v.write_val(reserved)?;
2194            }
2195            Message::StateInfo {
2196                time,
2197                uptime,
2198                downtime,
2199            } => {
2200                v.write_val(time)?;
2201                v.write_val(uptime)?;
2202                v.write_val(downtime)?;
2203            }
2204            Message::StateLocation {
2205                location,
2206                label,
2207                updated_at,
2208            } => {
2209                v.write_val(location)?;
2210                v.write_val(label)?;
2211                v.write_val(updated_at)?;
2212            }
2213            Message::StateGroup {
2214                group,
2215                label,
2216                updated_at,
2217            } => {
2218                v.write_val(group)?;
2219                v.write_val(label)?;
2220                v.write_val(updated_at)?;
2221            }
2222            Message::EchoRequest { payload } => {
2223                v.write_val(payload)?;
2224            }
2225            Message::EchoResponse { payload } => {
2226                v.write_val(payload)?;
2227            }
2228            Message::LightSetColor {
2229                reserved,
2230                color,
2231                duration,
2232            } => {
2233                v.write_val(reserved)?;
2234                v.write_val(color)?;
2235                v.write_val(duration)?;
2236            }
2237            Message::LightState {
2238                color,
2239                reserved,
2240                power,
2241                label,
2242                reserved2,
2243            } => {
2244                v.write_val(color)?;
2245                v.write_val(reserved)?;
2246                v.write_val(power)?;
2247                v.write_val(label)?;
2248                v.write_val(reserved2)?;
2249            }
2250            Message::LightSetPower { level, duration } => {
2251                v.write_val(if level > 0 { 65535u16 } else { 0u16 })?;
2252                v.write_val(duration)?;
2253            }
2254            Message::LightStatePower { level } => {
2255                v.write_val(level)?;
2256            }
2257            Message::LightStateHevCycle {
2258                duration,
2259                remaining,
2260                last_power,
2261            } => {
2262                v.write_val(duration)?;
2263                v.write_val(remaining)?;
2264                v.write_val(last_power)?;
2265            }
2266            Message::LightStateHevCycleConfiguration {
2267                indication,
2268                duration,
2269            } => {
2270                v.write_val(indication)?;
2271                v.write_val(duration)?;
2272            }
2273            Message::LightStateLastHevCycleResult { result } => {
2274                v.write_val(result)?;
2275            }
2276            Message::SetMultiZoneEffect {
2277                instance_id,
2278                typ,
2279                reserved,
2280                speed,
2281                duration,
2282                reserved7,
2283                reserved8,
2284                parameters,
2285            } => {
2286                v.write_val(instance_id)?;
2287                v.write_val(typ)?;
2288                v.write_val(reserved)?;
2289                v.write_val(speed)?;
2290                v.write_val(duration)?;
2291                v.write_val(reserved7)?;
2292                v.write_val(reserved8)?;
2293                v.write_val(&parameters)?;
2294            }
2295            Message::StateMultiZoneEffect {
2296                instance_id,
2297                typ,
2298                reserved,
2299                speed,
2300                duration,
2301                reserved7,
2302                reserved8,
2303                parameters,
2304            } => {
2305                v.write_val(instance_id)?;
2306                v.write_val(typ)?;
2307                v.write_val(reserved)?;
2308                v.write_val(speed)?;
2309                v.write_val(duration)?;
2310                v.write_val(reserved7)?;
2311                v.write_val(reserved8)?;
2312                v.write_val(&parameters)?;
2313            }
2314            Message::SetExtendedColorZones {
2315                duration,
2316                apply,
2317                zone_index,
2318                colors_count,
2319                colors,
2320            } => {
2321                v.write_val(duration)?;
2322                v.write_val(apply)?;
2323                v.write_val(zone_index)?;
2324                v.write_val(colors_count)?;
2325                v.write_val(&colors)?;
2326            }
2327            Message::StateExtendedColorZones {
2328                zones_count,
2329                zone_index,
2330                colors_count,
2331                colors,
2332            } => {
2333                v.write_val(zones_count)?;
2334                v.write_val(zone_index)?;
2335                v.write_val(colors_count)?;
2336                v.write_val(&colors)?;
2337            }
2338            Message::RelayGetPower { relay_index } => {
2339                v.write_val(relay_index)?;
2340            }
2341            Message::RelayStatePower { relay_index, level } => {
2342                v.write_val(relay_index)?;
2343                v.write_val(level)?;
2344            }
2345            Message::RelaySetPower { relay_index, level } => {
2346                v.write_val(relay_index)?;
2347                v.write_val(level)?;
2348            }
2349            Message::LightSetHevCycle { enable, duration } => {
2350                v.write_val(enable)?;
2351                v.write_val(duration)?;
2352            }
2353            Message::LightSetHevCycleConfiguration {
2354                indication,
2355                duration,
2356            } => {
2357                v.write_val(indication)?;
2358                v.write_val(duration)?;
2359            }
2360        }
2361
2362        let mut msg = RawMessage {
2363            frame,
2364            frame_addr: addr,
2365            protocol_header: phead,
2366            payload: v,
2367        };
2368
2369        msg.frame.size = msg.packed_size() as u16;
2370
2371        Ok(msg)
2372    }
2373
2374    /// The total size (in bytes) of the packed version of this message.
2375    pub fn packed_size(&self) -> usize {
2376        Frame::packed_size()
2377            + FrameAddress::packed_size()
2378            + ProtocolHeader::packed_size()
2379            + self.payload.len()
2380    }
2381
2382    /// Validates that this object was constructed correctly.  Panics if not.
2383    pub fn validate(&self) {
2384        self.frame.validate();
2385        self.frame_addr.validate();
2386        self.protocol_header.validate();
2387    }
2388
2389    /// Packs this RawMessage into some bytes that can be send over the network.
2390    ///
2391    /// The length of the returned data will be [RawMessage::packed_size] in size.
2392    pub fn pack(&self) -> Result<Vec<u8>, Error> {
2393        let mut v = Vec::with_capacity(self.packed_size());
2394        v.extend(self.frame.pack()?);
2395        v.extend(self.frame_addr.pack()?);
2396        v.extend(self.protocol_header.pack()?);
2397        v.extend(&self.payload);
2398        Ok(v)
2399    }
2400    /// Given some bytes (generally read from a network socket), unpack the data into a
2401    /// `RawMessage` structure.
2402    pub fn unpack(v: &[u8]) -> Result<RawMessage, Error> {
2403        let mut start = 0;
2404        let frame = Frame::unpack(v)?;
2405        frame.validate();
2406        start += Frame::packed_size();
2407        let addr = FrameAddress::unpack(&v[start..])?;
2408        addr.validate();
2409        start += FrameAddress::packed_size();
2410        let proto = ProtocolHeader::unpack(&v[start..])?;
2411        proto.validate();
2412        start += ProtocolHeader::packed_size();
2413
2414        let body = Vec::from(&v[start..(frame.size as usize)]);
2415
2416        Ok(RawMessage {
2417            frame,
2418            frame_addr: addr,
2419            protocol_header: proto,
2420            payload: body,
2421        })
2422    }
2423}
2424
2425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2426pub enum TemperatureRange {
2427    /// The device supports a range of temperatures
2428    Variable { min: u16, max: u16 },
2429    /// The device only supports 1 temperature
2430    Fixed(u16),
2431    /// For devices that aren't lighting products (the LIFX switch)
2432    None,
2433}
2434
2435#[derive(Clone, Debug, Copy, PartialEq, Eq)]
2436pub struct ProductInfo {
2437    pub name: &'static str,
2438
2439    /// The light changes physical appearance when the Hue value is changed
2440    pub color: bool,
2441
2442    /// The light supports emitting infrared light
2443    pub infrared: bool,
2444
2445    /// The light supports a 1D linear array of LEDs (the Z and Beam)
2446    pub multizone: bool,
2447
2448    /// The light may be connected to physically separated hardware (currently only the LIFX Tile)
2449    pub chain: bool,
2450
2451    /// The light supports emitted HEV light
2452    pub hev: bool,
2453
2454    /// The light supports a 2D matrix of LEDs (the Tile and Candle)
2455    pub matrix: bool,
2456
2457    /// The device has relays for controlling physical power to something (the LIFX switch)
2458    pub relays: bool,
2459
2460    /// The device has physical buttons to press (the LIFX switch)
2461    pub buttons: bool,
2462
2463    /// The temperature range this device supports
2464    pub temperature_range: TemperatureRange,
2465}
2466
2467/// Look up info about what a LIFX product supports.
2468///
2469/// You can get the vendor and product IDs from a bulb by receiving a [Message::StateVersion] message
2470///
2471/// Data is taken from <https://github.com/LIFX/products/blob/master/products.json>
2472#[rustfmt::skip]
2473pub fn get_product_info(vendor: u32, product: u32) -> Option<&'static ProductInfo> {
2474    match (vendor, product) {
2475        (1, 1) => Some(&ProductInfo { name: "LIFX Original 1000", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2476        (1, 3) => Some(&ProductInfo { name: "LIFX Color 650", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2477        (1, 10) => Some(&ProductInfo { name: "LIFX White 800 (Low Voltage)", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 6500 }  }),
2478        (1, 11) => Some(&ProductInfo { name: "LIFX White 800 (High Voltage)", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 6500 }  }),
2479        (1, 15) => Some(&ProductInfo { name: "LIFX Color 1000", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2480        (1, 18) => Some(&ProductInfo { name: "LIFX White 900 BR30 (Low Voltage)", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2481        (1, 19) => Some(&ProductInfo { name: "LIFX White 900 BR30 (High Voltage)", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2482        (1, 20) => Some(&ProductInfo { name: "LIFX Color 1000 BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2483        (1, 22) => Some(&ProductInfo { name: "LIFX Color 1000", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2484        (1, 27) => Some(&ProductInfo { name: "LIFX A19", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2485        (1, 28) => Some(&ProductInfo { name: "LIFX BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2486        (1, 29) => Some(&ProductInfo { name: "LIFX A19 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2487        (1, 30) => Some(&ProductInfo { name: "LIFX BR30 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2488        (1, 31) => Some(&ProductInfo { name: "LIFX Z", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2489        (1, 32) => Some(&ProductInfo { name: "LIFX Z", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2490        (1, 36) => Some(&ProductInfo { name: "LIFX Downlight", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2491        (1, 37) => Some(&ProductInfo { name: "LIFX Downlight", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2492        (1, 38) => Some(&ProductInfo { name: "LIFX Beam", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2493        (1, 39) => Some(&ProductInfo { name: "LIFX Downlight White to Warm", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2494        (1, 40) => Some(&ProductInfo { name: "LIFX Downlight", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2495        (1, 43) => Some(&ProductInfo { name: "LIFX A19", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2496        (1, 44) => Some(&ProductInfo { name: "LIFX BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2497        (1, 45) => Some(&ProductInfo { name: "LIFX A19 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2498        (1, 46) => Some(&ProductInfo { name: "LIFX BR30 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2499        (1, 49) => Some(&ProductInfo { name: "LIFX Mini Color", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2500        (1, 50) => Some(&ProductInfo { name: "LIFX Mini White to Warm", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 6500 }  }),
2501        (1, 51) => Some(&ProductInfo { name: "LIFX Mini White", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2502        (1, 52) => Some(&ProductInfo { name: "LIFX GU10", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2503        (1, 53) => Some(&ProductInfo { name: "LIFX GU10", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2504        (1, 55) => Some(&ProductInfo { name: "LIFX Tile", color: true, infrared: false, multizone: false, chain: true, hev: false, matrix: true, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2500, max: 9000 }  }),
2505        (1, 57) => Some(&ProductInfo { name: "LIFX Candle", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: true, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2506        (1, 59) => Some(&ProductInfo { name: "LIFX Mini Color", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2507        (1, 60) => Some(&ProductInfo { name: "LIFX Mini White to Warm", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 6500 }  }),
2508        (1, 61) => Some(&ProductInfo { name: "LIFX Mini White", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2509        (1, 62) => Some(&ProductInfo { name: "LIFX A19", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2510        (1, 63) => Some(&ProductInfo { name: "LIFX BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2511        (1, 64) => Some(&ProductInfo { name: "LIFX A19 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2512        (1, 65) => Some(&ProductInfo { name: "LIFX BR30 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2513        (1, 66) => Some(&ProductInfo { name: "LIFX Mini White", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2514        (1, 68) => Some(&ProductInfo { name: "LIFX Candle", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: true, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2515        (1, 70) => Some(&ProductInfo { name: "LIFX Switch", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: true, buttons: true, temperature_range: TemperatureRange::None }),
2516        (1, 71) => Some(&ProductInfo { name: "LIFX Switch", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: true, buttons: true, temperature_range: TemperatureRange::None }),
2517        (1, 81) => Some(&ProductInfo { name: "LIFX Candle White to Warm", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2200, max: 6500 }  }),
2518        (1, 82) => Some(&ProductInfo { name: "LIFX Filament Clear", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2100, max: 2100 }  }),
2519        (1, 85) => Some(&ProductInfo { name: "LIFX Filament Amber", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2000, max: 2000 }  }),
2520        (1, 87) => Some(&ProductInfo { name: "LIFX Mini White", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2521        (1, 88) => Some(&ProductInfo { name: "LIFX Mini White", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2522        (1, 89) => Some(&ProductInfo { name: "LIFX Switch", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: true, buttons: true, temperature_range: TemperatureRange::None }),
2523        (1, 90) => Some(&ProductInfo { name: "LIFX Clean", color: true, infrared: false, multizone: false, chain: false, hev: true, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2524        (1, 91) => Some(&ProductInfo { name: "LIFX Color", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2525        (1, 92) => Some(&ProductInfo { name: "LIFX Color", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2526        (1, 93) => Some(&ProductInfo { name: "LIFX A19 US", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2527        (1, 94) => Some(&ProductInfo { name: "LIFX BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2528        (1, 96) => Some(&ProductInfo { name: "LIFX Candle White to Warm", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2200, max: 6500 }  }),
2529        (1, 97) => Some(&ProductInfo { name: "LIFX A19", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2530        (1, 98) => Some(&ProductInfo { name: "LIFX BR30", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2531        (1, 99) => Some(&ProductInfo { name: "LIFX Clean", color: true, infrared: false, multizone: false, chain: false, hev: true, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2532        (1, 100) => Some(&ProductInfo { name: "LIFX Filament Clear", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2100, max: 2100 }  }),
2533        (1, 101) => Some(&ProductInfo { name: "LIFX Filament Amber", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2000, max: 2000 }  }),
2534        (1, 109) => Some(&ProductInfo { name: "LIFX A19 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2535        (1, 110) => Some(&ProductInfo { name: "LIFX BR30 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2536        (1, 111) => Some(&ProductInfo { name: "LIFX A19 Night Vision", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2537        (1, 112) => Some(&ProductInfo { name: "LIFX BR30 Night Vision Intl", color: true, infrared: true, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2538        (1, 113) => Some(&ProductInfo { name: "LIFX Mini WW US", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2539        (1, 114) => Some(&ProductInfo { name: "LIFX Mini WW Intl", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2540        (1, 115) => Some(&ProductInfo { name: "LIFX Switch", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: true, buttons: true, temperature_range: TemperatureRange::None }),
2541        (1, 116) => Some(&ProductInfo { name: "LIFX Switch", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: true, buttons: true, temperature_range: TemperatureRange::None }),
2542        (1, 117) => Some(&ProductInfo { name: "LIFX Z US", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2543        (1, 118) => Some(&ProductInfo { name: "LIFX Z Intl", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2544        (1, 119) => Some(&ProductInfo { name: "LIFX Beam US", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2545        (1, 120) => Some(&ProductInfo { name: "LIFX Beam Intl", color: true, infrared: false, multizone: true, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2546        (1, 123) => Some(&ProductInfo { name: "LIFX Color US", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2547        (1, 124) => Some(&ProductInfo { name: "LIFX Color Intl", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2548        (1, 125) => Some(&ProductInfo { name: "LIFX White to Warm US", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2549        (1, 126) => Some(&ProductInfo { name: "LIFX White to Warm Intl", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2550        (1, 127) => Some(&ProductInfo { name: "LIFX White US", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2551        (1, 128) => Some(&ProductInfo { name: "LIFX White Intl", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2552        (1, 129) => Some(&ProductInfo { name: "LIFX Color US", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2553        (1, 130) => Some(&ProductInfo { name: "LIFX Color Intl", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2554        (1, 131) => Some(&ProductInfo { name: "LIFX White To Warm US", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2555        (1, 132) => Some(&ProductInfo { name: "LIFX White To Warm Intl", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2556        (1, 133) => Some(&ProductInfo { name: "LIFX White US", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2557        (1, 134) => Some(&ProductInfo { name: "LIFX White Intl", color: false, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 2700, max: 2700 }  }),
2558        (1, 135) => Some(&ProductInfo { name: "LIFX GU10 Color US", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2559        (1, 136) => Some(&ProductInfo { name: "LIFX GU10 Color Intl", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: false, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2560        (1, 137) => Some(&ProductInfo { name: "LIFX Candle Color US", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: true, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2561        (1, 138) => Some(&ProductInfo { name: "LIFX Candle Color Intl", color: true, infrared: false, multizone: false, chain: false, hev: false, matrix: true, relays: false, buttons: false, temperature_range: TemperatureRange::Variable { min: 1500, max: 9000 }  }),
2562        (_, _) => None
2563    }
2564}
2565
2566#[cfg(test)]
2567mod tests {
2568    use super::*;
2569
2570    #[test]
2571    fn test_frame() {
2572        let frame = Frame {
2573            size: 0x1122,
2574            origin: 0,
2575            tagged: true,
2576            addressable: true,
2577            protocol: 1024,
2578            source: 1234567,
2579        };
2580        frame.validate();
2581
2582        let v = frame.pack().unwrap();
2583        println!("{:?}", v);
2584        assert_eq!(v[0], 0x22);
2585        assert_eq!(v[1], 0x11);
2586
2587        assert_eq!(v.len(), Frame::packed_size());
2588
2589        let unpacked = Frame::unpack(&v).unwrap();
2590        assert_eq!(frame, unpacked);
2591    }
2592
2593    #[test]
2594    fn test_decode_frame() {
2595        //             00    01    02    03    04    05    06    07
2596        let v = vec![0x28, 0x00, 0x00, 0x54, 0x42, 0x52, 0x4b, 0x52];
2597        let frame = Frame::unpack(&v).unwrap();
2598        println!("{:?}", frame);
2599
2600        // manual decoding:
2601        // size: 0x0028 ==> 40
2602        // 0x00, 0x54 (origin, tagged, addressable, protocol)
2603
2604        //  /-Origin ==> 0
2605        // || /- addressable=1
2606        // || |
2607        // 01010100 00000000
2608        //   |
2609        //   \- Tagged=0
2610
2611        assert_eq!(frame.size, 0x0028);
2612        assert_eq!(frame.origin, 1);
2613        assert!(frame.addressable);
2614        assert!(!frame.tagged);
2615        assert_eq!(frame.protocol, 1024);
2616        assert_eq!(frame.source, 0x524b5242);
2617    }
2618
2619    #[test]
2620    fn test_decode_frame1() {
2621        //             00    01    02    03    04    05    06    07
2622        let v = vec![0x24, 0x00, 0x00, 0x14, 0xca, 0x41, 0x37, 0x05];
2623        let frame = Frame::unpack(&v).unwrap();
2624        println!("{:?}", frame);
2625
2626        // 00010100 00000000
2627
2628        assert_eq!(frame.size, 0x0024);
2629        assert_eq!(frame.origin, 0);
2630        assert!(!frame.tagged);
2631        assert!(frame.addressable);
2632        assert_eq!(frame.protocol, 1024);
2633        assert_eq!(frame.source, 0x053741ca);
2634    }
2635
2636    #[test]
2637    fn test_frame_address() {
2638        let frame = FrameAddress {
2639            target: 0x11224488,
2640            reserved: [0; 6],
2641            reserved2: 0,
2642            ack_required: true,
2643            res_required: false,
2644            sequence: 248,
2645        };
2646        frame.validate();
2647
2648        let v = frame.pack().unwrap();
2649        assert_eq!(v.len(), FrameAddress::packed_size());
2650        println!("Packed FrameAddress: {:?}", v);
2651
2652        let unpacked = FrameAddress::unpack(&v).unwrap();
2653        assert_eq!(frame, unpacked);
2654    }
2655
2656    #[test]
2657    fn test_decode_frame_address() {
2658        //   1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
2659        let v = vec![
2660            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2661            0x01, 0x9c,
2662        ];
2663        assert_eq!(v.len(), FrameAddress::packed_size());
2664
2665        let frame = FrameAddress::unpack(&v).unwrap();
2666        frame.validate();
2667        println!("FrameAddress: {:?}", frame);
2668    }
2669
2670    #[test]
2671    fn test_protocol_header() {
2672        let frame = ProtocolHeader {
2673            reserved: 0,
2674            reserved2: 0,
2675            typ: 0x4455,
2676        };
2677        frame.validate();
2678
2679        let v = frame.pack().unwrap();
2680        assert_eq!(v.len(), ProtocolHeader::packed_size());
2681        println!("Packed ProtocolHeader: {:?}", v);
2682
2683        let unpacked = ProtocolHeader::unpack(&v).unwrap();
2684        assert_eq!(frame, unpacked);
2685    }
2686
2687    #[test]
2688    fn test_decode_protocol_header() {
2689        //   1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
2690        let v = vec![
2691            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
2692        ];
2693        assert_eq!(v.len(), ProtocolHeader::packed_size());
2694
2695        let frame = ProtocolHeader::unpack(&v).unwrap();
2696        frame.validate();
2697        println!("ProtocolHeader: {:?}", frame);
2698    }
2699
2700    #[test]
2701    fn test_decode_full() {
2702        let v = vec![
2703            0x24, 0x00, 0x00, 0x14, 0xca, 0x41, 0x37, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2704            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00, 0x00, 0x00,
2705            0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00,
2706        ];
2707
2708        let msg = RawMessage::unpack(&v).unwrap();
2709        msg.validate();
2710        println!("{:#?}", msg);
2711    }
2712
2713    #[test]
2714    fn test_decode_full_1() {
2715        let v = vec![
2716            0x58, 0x00, 0x00, 0x54, 0xca, 0x41, 0x37, 0x05, 0xd0, 0x73, 0xd5, 0x02, 0x97, 0xde,
2717            0x00, 0x00, 0x4c, 0x49, 0x46, 0x58, 0x56, 0x32, 0x00, 0xc0, 0x44, 0x30, 0xeb, 0x47,
2718            0xc4, 0x48, 0x18, 0x14, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
2719            0xb8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x4b, 0x69, 0x74, 0x63, 0x68, 0x65, 0x6e, 0x00,
2720            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2721            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2722            0x00, 0x00, 0x00, 0x00,
2723        ];
2724
2725        let msg = RawMessage::unpack(&v).unwrap();
2726        msg.validate();
2727        println!("{:#?}", msg);
2728    }
2729
2730    #[test]
2731    fn test_build_a_packet() {
2732        // packet taken from https://lan.developer.lifx.com/docs/building-a-lifx-packet
2733
2734        let msg = Message::LightSetColor {
2735            reserved: 0,
2736            color: HSBK {
2737                hue: 21845,
2738                saturation: 0xffff,
2739                brightness: 0xffff,
2740                kelvin: 3500,
2741            },
2742            duration: 1024,
2743        };
2744
2745        let raw = RawMessage::build(
2746            &BuildOptions {
2747                target: None,
2748                ack_required: false,
2749                res_required: false,
2750                sequence: 0,
2751                source: 0,
2752            },
2753            msg,
2754        )
2755        .unwrap();
2756
2757        let bytes = raw.pack().unwrap();
2758        println!("{:?}", bytes);
2759        assert_eq!(bytes.len(), 49);
2760        assert_eq!(
2761            bytes,
2762            vec![
2763                0x31, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2764                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2765                0x00, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0xFF, 0xFF, 0xFF,
2766                0xFF, 0xAC, 0x0D, 0x00, 0x04, 0x00, 0x00
2767            ]
2768        );
2769    }
2770
2771    #[test]
2772    fn test_lifx_string() {
2773        let s = CStr::from_bytes_with_nul(b"hello\0").unwrap();
2774        let ls = LifxString::new(s);
2775        assert_eq!(ls.cstr(), s);
2776        assert!(ls.cstr().to_bytes_with_nul().len() <= 32);
2777
2778        let s = CStr::from_bytes_with_nul(b"this is bigger than thirty two characters\0").unwrap();
2779        let ls = LifxString::new(s);
2780        assert_eq!(ls.cstr().to_bytes_with_nul().len(), 32);
2781        assert_eq!(
2782            ls.cstr(),
2783            CStr::from_bytes_with_nul(b"this is bigger than thirty two \0").unwrap()
2784        );
2785    }
2786
2787    #[test]
2788    fn test_lifx_decode_setextendedlightzones_msg() {
2789        let v = vec![
2790            0xbc, 0x02, 0x00, 0x14, 0x10, 0x00, 0x3e, 0x8f, 0xd0, 0x73, 0xd5, 0x6f, 0x20, 0xad,
2791            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x47, 0x00, 0x00, 0x00, 0x00,
2792            0x00, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0x01, 0x00,
2793            0x00, 0x10, 0x54, 0xf5, 0x8e, 0xc2, 0x95, 0x7b, 0xac, 0x0d, 0x0a, 0xf6, 0x3c, 0xca,
2794            0x7e, 0x78, 0xac, 0x0d, 0xc0, 0xf6, 0xea, 0xd1, 0x67, 0x75, 0xac, 0x0d, 0x76, 0xf7,
2795            0x98, 0xd9, 0x50, 0x72, 0xac, 0x0d, 0x2c, 0xf8, 0x46, 0xe1, 0x39, 0x6f, 0xac, 0x0d,
2796            0x21, 0xf2, 0xc1, 0xc5, 0xd8, 0x6f, 0xac, 0x0d, 0x15, 0xec, 0x3c, 0xaa, 0x76, 0x70,
2797            0xac, 0x0d, 0x0a, 0xe6, 0xb7, 0x8e, 0x14, 0x71, 0xac, 0x0d, 0xff, 0xdf, 0x32, 0x73,
2798            0xb2, 0x71, 0xac, 0x0d, 0x3d, 0xe1, 0xff, 0x5f, 0x8d, 0x73, 0xac, 0x0d, 0x7c, 0xe2,
2799            0xcc, 0x4c, 0x67, 0x75, 0xac, 0x0d, 0xba, 0xe3, 0x99, 0x39, 0x42, 0x77, 0xac, 0x0d,
2800            0xf9, 0xe4, 0x66, 0x26, 0x1c, 0x79, 0xac, 0x0d, 0x4e, 0xe2, 0x0a, 0x27, 0xbb, 0x79,
2801            0xac, 0x0d, 0xa4, 0xdf, 0xad, 0x27, 0x59, 0x7a, 0xac, 0x0d, 0xf9, 0xdc, 0x51, 0x28,
2802            0xf7, 0x7a, 0xac, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2803            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2804            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2805            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2806            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2807            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2808            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2809            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2810            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2811            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2812            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2813            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2814            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2815            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2816            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2817            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2818            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2819            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2820            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2821            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2822            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2823            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2824            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2825            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2826            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2827            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2828            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2829            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2830            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2831            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2832            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2833            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2834            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2835            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2836            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2837            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2838            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2839            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2840        ];
2841        let rawmsg = RawMessage::unpack(&v).unwrap();
2842        rawmsg.validate();
2843
2844        let msg = Message::from_raw(&rawmsg).unwrap();
2845
2846        match msg {
2847            Message::SetExtendedColorZones {
2848                duration: 1300,
2849                apply: ApplicationRequest::Apply,
2850                zone_index: 0,
2851                colors_count: 16,
2852                colors,
2853            } => {
2854                assert_eq!(colors.len(), 82);
2855            }
2856            _ => {
2857                panic!("Unexpected message")
2858            }
2859        }
2860    }
2861
2862    #[test]
2863    fn test_lifx_decode_setmultizoneeffect_message() {
2864        let v = vec![
2865            0x5f, 0x00, 0x00, 0x14, 0x10, 0x00, 0x3e, 0x8f, 0xd0, 0x73, 0xd5, 0x6f, 0x20, 0xad,
2866            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x9a, 0x00, 0x00, 0x00, 0x00,
2867            0x00, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
2868            0x00, 0xb8, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2869            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2870            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2871            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2872        ];
2873        let rawmsg = RawMessage::unpack(&v).unwrap();
2874        rawmsg.validate();
2875
2876        let msg = Message::from_raw(&rawmsg).unwrap();
2877
2878        assert!(
2879            msg == Message::SetMultiZoneEffect {
2880                instance_id: 0,
2881                typ: MultiZoneEffectType::Move,
2882                reserved: 0,
2883                speed: 3000,
2884                duration: 0,
2885                reserved7: 0,
2886                reserved8: 0,
2887                parameters: [0, 0, 1, 0, 0, 0, 0, 0,],
2888            }
2889        )
2890    }
2891}