lifxi 0.1.1

Control LIFX devices.
Documentation
use std::fmt;
use std::num::{ParseFloatError, ParseIntError};
use std::str::FromStr;
use std::time::Duration as StdDuration;

use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};

/// Specifies the desired color setting of a light.
///
/// HSBK is the preferred method of specifying colors (RGB represents color poorly); as such,
/// `Hue`, `Saturation`, `Brightness`, and `Kelvin` are among the more useful variants here.
///
/// RGB colors will automatically be converted by the API.
#[derive(Clone, Debug, PartialEq)]
pub enum Color {
    /// Sets the hue and saturation components necessary to change the color to red, leaving
    /// brightness untouched.
    Red,
    /// Sets the hue and saturation components necessary to change the color to orange, leaving
    /// brightness untouched.
    Orange,
    /// Sets the hue and saturation components necessary to change the color to yellow, leaving
    /// brightness untouched.
    Yellow,
    /// Sets the hue and saturation components necessary to change the color to green, leaving
    /// brightness untouched.
    Green,
    /// Sets the hue and saturation components necessary to change the color to blue, leaving
    /// brightness untouched.
    Blue,
    /// Sets the hue and saturation components necessary to change the color to purple, leaving
    /// brightness untouched.
    Purple,
    /// Sets the hue and saturation components necessary to change the color to pink, leaving
    /// brightness untouched.
    Pink,
    /// Sets the hue and saturation components necessary to change the color to white, leaving
    /// brightness untouched.
    White,
    /// Sets the hue, leaving all else untouched.
    ///
    /// The hue should be between 0 and 360.
    Hue(u16),
    /// Sets the saturation, leaving all else untouched.
    ///
    /// The saturation should be between 0 and 1.
    Saturation(f32),
    /// Sets the brightness, leaving all else untouched.
    ///
    /// The brightness should be between 0 and 1.
    Brightness(f32),
    /// Sets the temperature to the given value and saturation to 0, leaving all else untouched.
    ///
    /// The temperature should be between 1500 and 9000.
    Kelvin(u16),
    /// Used to specify more than one of hue, saturation, brightness, and color temperature.
    ///
    /// See `Hue`, `Saturation`, `Brightness`, and `Kelvin`. `None` values are ignored.
    Hsbk(Option<u16>, Option<f32>, Option<f32>, Option<u16>),
    /// Sets the color to an RGB color using the given numeric components.
    ///
    /// It is preferred to use this over `RgbStr` where posssible.
    Rgb([u8; 3]),
    /// Sets the color to an RGB color using the given specifier string.
    ///
    /// Strings may be of the form `#ff0000` or `ff0000`; outputs will be normalized to the former.
    ///
    /// It is preferred to use [`Rgb`](#variant.Rgb) instead of this where posssible.
    RgbStr(String),
    /// Uses a custom specifier string.
    ///
    /// This option exists for undocumented features. For instance, "cyan" is a valid color choice,
    /// but it is undocumented and therefore (theoretically) unstable, so it is not officially/
    /// supported by this crate.
    Custom(String),
}

impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Color::Red => write!(f, "red"),
            Color::Orange => write!(f, "orange"),
            Color::Yellow => write!(f, "yellow"),
            Color::Green => write!(f, "green"),
            Color::Blue => write!(f, "blue"),
            Color::Purple => write!(f, "purple"),
            Color::Pink => write!(f, "pink"),
            Color::White => write!(f, "white"),
            Color::Hue(hue) => write!(f, "hue:{}", hue),
            Color::Saturation(sat) => write!(f, "saturation:{}", sat),
            Color::Brightness(b) => write!(f, "brightness:{}", b),
            Color::Kelvin(t) => write!(f, "kelvin:{}", t),
            Color::Hsbk(hue, sat, bright, kelvin) => {
                let hue = hue.map(|hue| format!("hue:{}", hue));
                let sat = sat.map(|saturation| format!("saturation:{}", saturation));
                let bright = bright.map(|brightness| format!("brightness:{}", brightness));
                let kelvin = kelvin.map(|kelvin| format!("kelvin:{}", kelvin));
                let vec: Vec<_> = [hue, sat, bright, kelvin]
                    .into_iter()
                    .cloned()
                    .filter_map(|c| c)
                    .collect();
                let s = vec.join(" ");
                write!(f, "{}", s)
            }
            Color::Rgb(rgb) => write!(f, "rgb:{},{},{}", rgb[0], rgb[1], rgb[2]),
            Color::RgbStr(s) => {
                if s.starts_with('#') {
                    write!(f, "{}", s)
                } else {
                    write!(f, "#{}", s)
                }
            }
            Color::Custom(s) => write!(f, "{}", s),
        }
    }
}

/// Represents an error encountered while deserializing a color.
#[derive(Clone, Debug, PartialEq)]
pub enum ColorParseError {
    /// No hue was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "hue:".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoHue));
    /// ```
    NoHue,
    /// The hue could not be parsed as an integer.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "hue:j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericHue(ParseIntError),
    /// No saturation was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "saturation:".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoSaturation));
    /// ```
    NoSaturation,
    /// The saturation could not be parsed as a float.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "saturation:j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericSaturation(ParseFloatError),
    /// No brightness was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "brightness:".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoBrightness));
    /// ```
    NoBrightness,
    /// The brightness could not be parsed as a float.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "brightness:j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericBrightness(ParseFloatError),
    /// No color temperature was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "kelvin:".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoKelvin));
    /// ```
    NoKelvin,
    /// The color temperature could not be parsed as an integer.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "kelvin:j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericKelvin(ParseIntError),
    /// When parsing our way through what looked like an HSBK color, we found another color.
    ///
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "hue:100 rgb:0,0,0".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    WeirdHsbkComponent(Color),
    /// Multiple hues were specified.
    ///
    /// ##
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "hue:100 hue:100".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::MultipleHues));
    /// ```
    MultipleHues,
    /// Multiple hues were specified.
    ///
    /// ##
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "saturation:100 saturation:100".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::MultipleSaturations));
    /// ```
    MultipleSaturations,
    /// Multiple hues were specified.
    ///
    /// ##
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "brightness:0.4 brightness:0.4".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::MultipleBrightnesses));
    /// ```
    MultipleBrightnesses,
    /// Multiple hues were specified.
    ///
    /// ##
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "kelvin:2000 kelvin:2000".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::MultipleKelvins));
    /// ```
    MultipleKelvins,
    /// No red component was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoRed));
    /// ```
    NoRed,
    /// The red component could not be parsed as an integer.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericRed(ParseIntError),
    /// No green component was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:0,".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoGreen));
    /// ```
    NoGreen,
    /// The green component could not be parsed as an integer.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:0,j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericGreen(ParseIntError),
    /// No blue component was given.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:0,1,".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::NoBlue));
    /// ```
    NoBlue,
    /// The blue component could not be parsed as an integer.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "rgb:0,1,j".parse::<Color>();
    /// assert!(color.is_err());
    /// ```
    NonNumericBlue(ParseIntError),
    /// The string is too short to be an RGB string and was not recognized as a keyword.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "foo".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::ShortString));
    /// ```
    ShortString,
    /// The string is too long to be an RGB string and was not recognized as a keyword.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let color = "foobarbaz".parse::<Color>();
    /// assert_eq!(color, Err(ColorParseError::LongString));
    /// ```
    LongString,
}

impl fmt::Display for ColorParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::ColorParseError::*;
        match self {
            NoHue => write!(f, "Expected hue after hue: label."),
            NonNumericHue(e) => write!(f, "Failed to parse hue as integer: {}", e),
            NoSaturation => write!(f, "Expected saturation after saturation: label."),
            NonNumericSaturation(e) => write!(f, "Failed to parse saturation as float: {}", e),
            NoBrightness => write!(f, "Expected brightness after brightness: label."),
            NonNumericBrightness(e) => write!(f, "Failed to parse brightness as float: {}", e),
            NoKelvin => write!(f, "Expected color temperature after kelvin: label."),
            NonNumericKelvin(e) => write!(f, "Failed to parse color temperature as integer: {}", e),
            WeirdHsbkComponent(c) => write!(f, "Found another color while parsing as HSBK: {}", c),
            MultipleHues => write!(f, "Encountered multiple hue specifications."),
            MultipleSaturations => write!(f, "Encountered multiple saturation specifications."),
            MultipleBrightnesses => write!(f, "Encountered multiple brightness specifications."),
            MultipleKelvins => write!(f, "Encountered multiple color temperature specifications."),
            NoRed => write!(f, "Expected red component after rgb: label."),
            NonNumericRed(e) => write!(f, "Failed to parse red component as integer: {}", e),
            NoGreen => write!(f, "Expected green component after comma."),
            NonNumericGreen(e) => write!(f, "Failed to parse green component as integer: {}", e),
            NoBlue => write!(f, "Expected blue component after comma."),
            NonNumericBlue(e) => write!(f, "Failed to parse blue component as integer: {}", e),
            ShortString => write!(
                f,
                "String is too short to be an RGB string and was not recognized as a keyword."
            ),
            LongString => write!(
                f,
                "String is too long to be an RGB string and was not recognized as a keyword."
            ),
        }
    }
}

impl FromStr for Color {
    type Err = ColorParseError;
    /// Parses the color string into a color setting.
    ///
    /// ## Notes
    /// Custom colors cannot be made with this method; use `Color::Custom(s)` instead.
    #[allow(clippy::cyclomatic_complexity)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use self::Color::*;
        use self::ColorParseError::*;
        match s {
            "red" => Ok(Red),
            "orange" => Ok(Orange),
            "yellow" => Ok(Yellow),
            "green" => Ok(Green),
            "blue" => Ok(Blue),
            "purple" => Ok(Purple),
            "pink" => Ok(Pink),
            "white" => Ok(White),
            s if s.split(' ').count() > 1 => {
                let mut hue = None;
                let mut sat = None;
                let mut bright = None;
                let mut kelvin = None;
                for part in s.split(' ') {
                    let color = part.parse::<Self>()?;
                    match color {
                        Hue(h) => {
                            if hue.replace(h).is_some() {
                                return Err(MultipleHues);
                            }
                        }
                        Saturation(s) => {
                            if sat.replace(s).is_some() {
                                return Err(MultipleSaturations);
                            }
                        }
                        Brightness(b) => {
                            if bright.replace(b).is_some() {
                                return Err(MultipleBrightnesses);
                            }
                        }
                        Kelvin(k) => {
                            if kelvin.replace(k).is_some() {
                                return Err(MultipleKelvins);
                            }
                        }
                        c => {
                            return Err(WeirdHsbkComponent(c));
                        }
                    }
                }
                Ok(Hsbk(hue, sat, bright, kelvin))
            }
            hue if hue.starts_with("hue:") => {
                if let Some(spec) = hue.split(':').nth(1) {
                    if spec.trim().is_empty() {
                        Err(NoHue)
                    } else {
                        let hue = spec.parse();
                        hue.map(Hue).map_err(NonNumericHue)
                    }
                } else {
                    Err(NoHue)
                }
            }
            s if s.starts_with("saturation:") => {
                if let Some(spec) = s.split(':').nth(1) {
                    if spec.trim().is_empty() {
                        Err(NoSaturation)
                    } else {
                        let s = spec.parse();
                        s.map(Saturation).map_err(NonNumericSaturation)
                    }
                } else {
                    Err(NoSaturation)
                }
            }
            b if b.starts_with("brightness:") => {
                if let Some(spec) = b.split(':').nth(1) {
                    if spec.trim().is_empty() {
                        Err(NoBrightness)
                    } else {
                        let b = spec.parse();
                        b.map(Brightness).map_err(NonNumericBrightness)
                    }
                } else {
                    Err(NoBrightness)
                }
            }
            k if k.starts_with("kelvin:") => {
                if let Some(spec) = k.split(':').nth(1) {
                    if spec.trim().is_empty() {
                        Err(NoKelvin)
                    } else {
                        let k = spec.parse();
                        k.map(Kelvin).map_err(NonNumericKelvin)
                    }
                } else {
                    Err(NoKelvin)
                }
            }
            // Let's revisit this with combinators and Try later.
            r if r.starts_with("rgb:") => {
                let mut split = r.split(':');
                if let Some(parts) = split.nth(1) {
                    let mut parts = parts.split(',');
                    if let Some(r) = parts.next() {
                        if r.trim().is_empty() {
                            return Err(NoRed);
                        }
                        if let Some(g) = parts.next() {
                            if g.trim().is_empty() {
                                return Err(NoGreen);
                            }
                            if let Some(b) = parts.next() {
                                if b.trim().is_empty() {
                                    return Err(NoBlue);
                                }
                                match r.parse() {
                                    Ok(r) => match g.parse() {
                                        Ok(g) => match b.parse() {
                                            Ok(b) => Ok(Rgb([r, g, b])),
                                            Err(e) => Err(NonNumericBlue(e)),
                                        },
                                        Err(e) => Err(NonNumericGreen(e)),
                                    },
                                    Err(e) => Err(NonNumericRed(e)),
                                }
                            } else {
                                Err(NoBlue)
                            }
                        } else {
                            Err(NoGreen)
                        }
                    } else {
                        Err(NoRed)
                    }
                } else {
                    Err(NoRed)
                }
            }
            s => {
                if s.starts_with('#') {
                    match s.len() {
                        x if x < 7 => Err(ShortString),
                        7 => Ok(RgbStr(s.to_string())),
                        _ => Err(LongString),
                    }
                } else {
                    match s.len() {
                        x if x < 6 => Err(ShortString),
                        6 => Ok(RgbStr(s.to_string())),
                        _ => Err(LongString),
                    }
                }
            }
        }
    }
}

/// Represents a (local) color validation error.
#[derive(Debug, PartialEq)]
pub enum Error {
    /// The given hue was greater than the maximum hue of 360.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Hue(361).validate();
    /// assert_eq!(res, Err(ColorValidationError::Hue(361)));
    /// ```
    Hue(u16),
    /// The given saturation was greater than 1.0.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Saturation(1.1).validate();
    /// assert_eq!(res, Err(ColorValidationError::SaturationHigh(1.1)));
    /// ```
    SaturationHigh(f32),
    /// The given saturation was less than 0.0.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Saturation(-0.1).validate();
    /// assert_eq!(res, Err(ColorValidationError::SaturationLow(-0.1)));
    /// ```
    SaturationLow(f32),
    /// The given brightness was greater than 1.0.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Brightness(1.1).validate();
    /// assert_eq!(res, Err(ColorValidationError::BrightnessHigh(1.1)));
    /// ```
    BrightnessHigh(f32),
    /// The given brightness was less than 0.0.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Brightness(-0.1).validate();
    /// assert_eq!(res, Err(ColorValidationError::BrightnessLow(-0.1)));
    /// ```
    BrightnessLow(f32),
    /// The given temperature was greater than 9000 K.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Kelvin(9001).validate();
    /// assert_eq!(res, Err(ColorValidationError::KelvinHigh(9001)));
    /// ```
    KelvinHigh(u16),
    /// The given temperature was less than 1500 K.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Kelvin(1499).validate();
    /// assert_eq!(res, Err(ColorValidationError::KelvinLow(1499)));
    /// ```
    KelvinLow(u16),
    /// None of hue, saturation, brightness, or color temperature were specified, so this is an
    /// empty color.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::Hsbk(None, None, None, None).validate();
    /// assert_eq!(res, Err(ColorValidationError::HsbkEmpty));
    HsbkEmpty,
    /// The given RGB string was too short.
    ///
    /// ## Examples
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::RgbStr("12345".to_string()).validate();
    /// assert_eq!(res, Err(ColorValidationError::RgbStrShort(false, "12345".to_string())));
    /// let res = Color::RgbStr("#12345".to_string()).validate();
    /// assert_eq!(res, Err(ColorValidationError::RgbStrShort(true, "#12345".to_string())));
    /// ```
    RgbStrShort(bool, String),
    /// The given RGB string was too long.
    ///
    /// ## Examples
    /// ```
    /// use lifxi::http::prelude::*;
    /// let res = Color::RgbStr("1234567".to_string()).validate();
    /// assert_eq!(res, Err(ColorValidationError::RgbStrLong(false, "1234567".to_string())));
    /// let res = Color::RgbStr("#1234567".to_string()).validate();
    /// assert_eq!(res, Err(ColorValidationError::RgbStrLong(true, "#1234567".to_string())));
    /// ```
    RgbStrLong(bool, String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::Hue(hue) => write!(f, "Hue {} is too large (max: 360).", hue),
            Error::SaturationHigh(sat) => write!(f, "Saturation {} is too large (max: 1.0).", sat),
            Error::SaturationLow(sat) => write!(f, "Saturation {} is negative.", sat),
            Error::BrightnessHigh(b) => write!(f, "Brightness {} is too large (max: 1.0).", b),
            Error::BrightnessLow(b) => write!(f, "Brightness {} is negative.", b),
            Error::KelvinHigh(t) => write!(f, "Temperature {} K is too large (max: 9000 K).", t),
            Error::KelvinLow(t) => write!(f, "Temperature {} K is too small (min: 1500 K).", t),
            Error::HsbkEmpty => write!(
                f,
                "No hue, saturation, brightness, or color temperature given."
            ),
            Error::RgbStrShort(h, s) => write!(
                f,
                "RGB string {} is too short ({} chars; expected {}).",
                s,
                s.len(),
                if *h { 7 } else { 6 }
            ),
            Error::RgbStrLong(h, s) => write!(
                f,
                "RGB string {} is too long ({} chars; expected {}).",
                s,
                s.len(),
                if *h { 7 } else { 6 }
            ),
        }
    }
}

impl ::std::error::Error for Error {}

impl Color {
    /// Checks whether the color is valid.
    ///
    /// ## Notes
    /// Custom color strings are not validated.
    ///
    /// ## Examples
    /// ```
    /// use lifxi::http::Color;
    /// // Too short
    /// let setting = Color::RgbStr("".to_string());
    /// assert!(setting.validate().is_err());
    /// // Too long for no leading #
    /// let setting = Color::RgbStr("1234567".to_string());
    /// assert!(setting.validate().is_err());
    /// // Too high (max 9000)
    /// let setting = Color::Kelvin(10_000);
    /// assert!(setting.validate().is_err());
    /// // Too high (max 1.0)
    /// let setting = Color::Brightness(1.2);
    /// assert!(setting.validate().is_err());
    /// // Too low (min 0.0)
    /// let setting = Color::Saturation(-0.1);
    /// assert!(setting.validate().is_err());
    /// let setting = Color::Kelvin(2_000);
    /// assert!(setting.validate().is_ok());
    /// ```
    pub fn validate(&self) -> Result<(), Error> {
        use self::Color::*;
        use self::Error::*;
        match self {
            Red | Orange | Yellow | Green | Blue | Purple | Pink | White | Rgb(_) | Custom(_) => {
                Ok(())
            }
            self::Color::Hue(hue) => {
                if *hue > 360 {
                    Err(self::Error::Hue(*hue))
                } else {
                    Ok(())
                }
            }
            Saturation(s) => {
                if *s > 1.0 {
                    Err(SaturationHigh(*s))
                } else if *s < 0.0 {
                    Err(SaturationLow(*s))
                } else {
                    Ok(())
                }
            }
            Brightness(b) => {
                if *b > 1.0 {
                    Err(BrightnessHigh(*b))
                } else if *b < 0.0 {
                    Err(BrightnessLow(*b))
                } else {
                    Ok(())
                }
            }
            Kelvin(t) => {
                if *t < 1500 {
                    Err(KelvinLow(*t))
                } else if *t > 9000 {
                    Err(KelvinHigh(*t))
                } else {
                    Ok(())
                }
            }
            RgbStr(s) => {
                if s.starts_with('#') {
                    if s.len() > 7 {
                        Err(RgbStrLong(true, s.clone()))
                    } else if s.len() < 7 {
                        Err(RgbStrShort(true, s.clone()))
                    } else {
                        Ok(())
                    }
                } else if s.len() > 6 {
                    Err(RgbStrLong(false, s.clone()))
                } else if s.len() < 6 {
                    Err(RgbStrShort(false, s.clone()))
                } else {
                    Ok(())
                }
            }
            Hsbk(h, s, b, k) => {
                if h.is_none() && s.is_none() && b.is_none() && k.is_none() {
                    Err(HsbkEmpty)
                } else {
                    if let Some(h) = h {
                        Color::Hue(*h).validate()?;
                    }
                    if let Some(s) = s {
                        Color::Saturation(*s).validate()?;
                    }
                    if let Some(b) = b {
                        Color::Brightness(*b).validate()?;
                    }
                    if let Some(k) = k {
                        Color::Kelvin(*k).validate()?;
                    }
                    Ok(())
                }
            }
        }
    }
}

/// A thin wrapper for `std::time::Duration` to aid with {de,}serialization.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Duration(StdDuration);

impl From<StdDuration> for Duration {
    fn from(duration: StdDuration) -> Self {
        Duration(duration)
    }
}

/// A wrapper around a power state to make sure it is serialized properly.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Power(bool);

impl From<bool> for Power {
    fn from(on: bool) -> Self {
        Power(on)
    }
}

/// Encodes a desired final state.
///
/// This struct should only be used directly when using
/// [`Selected::set_states`](struct.Selected.html#method.set_states), and even then, it is
/// encouraged to use the builder methods instead of directly constructing a set of changes.
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct State {
    /// The desired power state, if appropriate.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power: Option<Power>,
    /// The desired color setting, if appropriate.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<Color>,
    /// The desired brightness level (0–1), if appropriate. Will take priority over any brightness
    /// specified in a color setting.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brightness: Option<f32>,
    /// How long the transition should take.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration: Option<Duration>,
    /// If appropriate, the desired infrared light level (0–1).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub infrared: Option<f32>,
}

impl<'de> Deserialize<'de> for Color {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<(Self), D::Error> {
        let s = String::deserialize(deserializer)?;
        s.parse::<Self>().map_err(DeError::custom)
    }
}

impl Serialize for Color {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&format!("{}", self))
    }
}

impl Serialize for Duration {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let time = self.0;
        let secs = time.as_secs() as f64;
        let millis = f64::from(time.subsec_millis()) / 1000.0;
        let t = secs + millis;
        serializer.serialize_f64(t)
    }
}

impl<'de> Deserialize<'de> for Duration {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<(Self), D::Error> {
        f64::deserialize(deserializer).map(|f| {
            let secs = f.floor() as u64;
            let millis = ((f % 1.0) * 1000.0) as u32;
            Duration(StdDuration::new(secs, millis * 1_000_000))
        })
    }
}
impl Serialize for Power {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let on = self.0;
        serializer.serialize_str(if on { "on" } else { "off" })
    }
}

impl<'de> Deserialize<'de> for Power {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<(Self), D::Error> {
        let s = String::deserialize(deserializer)?;
        if s == "on" {
            Ok(Power(true))
        } else {
            Ok(Power(false))
        }
    }
}

impl State {
    /// Constructs an empty state.
    ///
    /// Identical to [`State::builder`](#method.builder).
    pub fn new() -> Self {
        Self::default()
    }
    /// Creates a new builder.
    pub fn builder() -> Self {
        Self::default()
    }
    /// Builder function to set target power setting.
    ///
    /// ## Example
    /// ```
    /// use std::time::Duration;
    /// use lifxi::http::State;
    /// let new: State = State::builder().power(true).transition(Duration::from_millis(800));
    /// ```
    pub fn power<P: Into<Power>>(mut self, on: P) -> Self {
        self.power = Some(on.into());
        self
    }
    /// Builder function to set target color setting.
    ///
    /// ## Example
    /// ```
    /// use std::time::Duration;
    /// use lifxi::http::{Color::*, State};
    /// let new: State = State::builder().color(Red);
    /// ```
    pub fn color(mut self, color: Color) -> Self {
        self.color = Some(color);
        self
    }
    /// Builder function to set target brightness setting.
    ///
    /// ## Example
    /// ```
    /// use std::time::Duration;
    /// use lifxi::http::State;
    /// let new: State = State::builder().brightness(0.7).transition(Duration::from_millis(800));
    /// ```
    pub fn brightness(mut self, brightness: f32) -> Self {
        self.brightness = Some(brightness);
        self
    }
    /// Builder function to set animation duration.
    ///
    /// ## Example
    /// ```
    /// use std::time::Duration;
    /// use lifxi::http::{Color::*, State};
    /// let new: State = State::builder().color(Red).transition(Duration::from_millis(800));
    /// ```
    pub fn transition<D: Into<Duration>>(mut self, duration: D) -> Self {
        self.duration = Some(duration.into());
        self
    }
    /// Builder function to set target maximum infrared level.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::State;
    /// let new: State = State::builder().infrared(0.8);
    /// ```
    pub fn infrared(mut self, infrared: f32) -> Self {
        self.infrared = Some(infrared);
        self
    }
}

/// Encodes a desired state change.
///
/// This struct is intended for use with
/// [`Selected::change_state`](struct.Selected.html#method.change_state), and it is encouraged to
/// use the builder methods instead of directly constructing a changeset.
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct StateChange {
    /// The desired power state.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power: Option<Power>,
    /// How long the transition should take.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration: Option<Duration>,
    /// The desired change in infrared light level.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub infrared: Option<f32>,
    /// The desired change in hue.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hue: Option<i16>,
    /// The desired change in saturation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub saturation: Option<f32>,
    /// The desired change in brightness.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brightness: Option<f32>,
    /// The desired change in color temperature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kelvin: Option<i16>,
}

impl StateChange {
    /// Constructs an empty state change.
    ///
    /// Identical to [`StateChange::new`](#method.builder).
    pub fn new() -> Self {
        Self::builder()
    }
    /// Creates a new builder.
    pub fn builder() -> Self {
        Self::default()
    }
    /// Builder function to change target power state.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().power(true);
    /// ```
    pub fn power<P: Into<Power>>(mut self, on: P) -> Self {
        self.power = Some(on.into());
        self
    }
    /// Builder function to change transition duration.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().transition(::std::time::Duration::from_secs(1));
    /// ```
    pub fn transition<T: Into<Duration>>(mut self, duration: T) -> Self {
        self.duration = Some(duration.into());
        self
    }
    /// Builder function to set target change in hue.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().hue(-60);
    /// ```
    pub fn hue(mut self, hue: i16) -> Self {
        self.hue = Some(hue);
        self
    }
    /// Builder function to set target change in saturation.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().saturation(-0.1);
    /// ```
    pub fn saturation(mut self, saturation: f32) -> Self {
        self.saturation = Some(saturation);
        self
    }
    /// Builder function to set target change in brightness.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().brightness(0.4);
    /// ```
    pub fn brightness(mut self, brightness: f32) -> Self {
        self.brightness = Some(brightness);
        self
    }
    /// Builder function to set target change in color temperature.
    ///
    /// ## Example
    /// ```
    /// use lifxi::http::StateChange;
    /// let new: StateChange = StateChange::builder().kelvin(-200);
    /// ```
    pub fn kelvin(mut self, temp: i16) -> Self {
        self.kelvin = Some(temp);
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    mod state {
        use super::*;
        #[test]
        fn builder() {
            let state = State::new()
                .power(true)
                .transition(::std::time::Duration::from_secs(1))
                .color(Color::White)
                .infrared(0.7)
                .brightness(0.3);
            assert_eq!(state.power, Some(Power(true)));
            assert_eq!(state.duration.map(|d| d.0.as_secs()), Some(1));
            assert_eq!(state.brightness, Some(0.3));
            assert_eq!(state.infrared, Some(0.7));
            assert_eq!(
                state.color.map(|c| format!("{}", c)),
                Some("white".to_string())
            );
        }
        mod change {
            use super::*;
            #[test]
            fn builder() {
                let change = StateChange::new()
                    .power(true)
                    .transition(::std::time::Duration::from_secs(3))
                    .hue(120)
                    .saturation(-0.3)
                    .brightness(0.1)
                    .kelvin(500);
                assert_eq!(change.power, Some(Power(true)));
                assert_eq!(change.duration.map(|d| d.0.as_secs()), Some(3));
                assert_eq!(change.hue, Some(120));
                assert_eq!(change.saturation, Some(-0.3));
                assert_eq!(change.brightness, Some(0.1));
                assert_eq!(change.kelvin, Some(500));
            }
        }
    }
    mod color {
        use super::*;
        #[test]
        fn serialize() {
            let color = Color::Red;
            assert_eq!(&format!("{}", color), "red");
            let color = Color::Orange;
            assert_eq!(&format!("{}", color), "orange");
            let color = Color::Yellow;
            assert_eq!(&format!("{}", color), "yellow");
            let color = Color::Green;
            assert_eq!(&format!("{}", color), "green");
            let color = Color::Blue;
            assert_eq!(&format!("{}", color), "blue");
            let color = Color::Purple;
            assert_eq!(&format!("{}", color), "purple");
            let color = Color::Pink;
            assert_eq!(&format!("{}", color), "pink");
            let color = Color::White;
            assert_eq!(&format!("{}", color), "white");
            let color = Color::Custom("cyan".to_string());
            assert_eq!(&format!("{}", color), "cyan");
            let color = Color::Hue(240);
            assert_eq!(&format!("{}", color), "hue:240");
            let color = Color::Saturation(0.531);
            assert_eq!(&format!("{}", color), "saturation:0.531");
            let color = Color::Brightness(0.3);
            assert_eq!(&format!("{}", color), "brightness:0.3");
            let color = Color::Kelvin(3500);
            assert_eq!(&format!("{}", color), "kelvin:3500");
            let color = Color::Hsbk(Some(240), Some(0.531), Some(0.3), Some(3500));
            assert_eq!(
                &format!("{}", color),
                "hue:240 saturation:0.531 brightness:0.3 kelvin:3500"
            );
            let color = Color::Rgb([0, 17, 36]);
            assert_eq!(&format!("{}", color), "rgb:0,17,36");
            let color = Color::RgbStr("123456".to_string());
            assert_eq!(&format!("{}", color), "#123456");
            let color = Color::RgbStr("#000000".to_string());
            assert_eq!(&format!("{}", color), "#000000");
        }
        #[test]
        fn deserialize() {
            let color = "red".parse();
            assert_eq!(color, Ok(Color::Red));
            let color = "orange".parse();
            assert_eq!(color, Ok(Color::Orange));
            let color = "yellow".parse();
            assert_eq!(color, Ok(Color::Yellow));
            let color = "green".parse();
            assert_eq!(color, Ok(Color::Green));
            let color = "blue".parse();
            assert_eq!(color, Ok(Color::Blue));
            let color = "purple".parse();
            assert_eq!(color, Ok(Color::Purple));
            let color = "pink".parse();
            assert_eq!(color, Ok(Color::Pink));
            let color = "white".parse();
            assert_eq!(color, Ok(Color::White));
            let color = "cyan".parse::<Color>();
            assert!(color.is_err());
            let color = "hue:240".parse();
            assert_eq!(color, Ok(Color::Hue(240)));
            let color = "saturation:0.531".parse();
            assert_eq!(color, Ok(Color::Saturation(0.531)));
            let color = "brightness:0.3".parse();
            assert_eq!(color, Ok(Color::Brightness(0.3)));
            let color = "kelvin:3500".parse();
            assert_eq!(color, Ok(Color::Kelvin(3500)));
            let color = "hue:240 saturation:0.531 brightness:0.3 kelvin:3500".parse();
            assert_eq!(
                color,
                Ok(Color::Hsbk(Some(240), Some(0.531), Some(0.3), Some(3500)))
            );
            let color = "saturation:0.531 brightness:0.3 kelvin:3500".parse();
            assert_eq!(
                color,
                Ok(Color::Hsbk(None, Some(0.531), Some(0.3), Some(3500)))
            );
            let color = "hue:240 brightness:0.3 kelvin:3500".parse();
            assert_eq!(
                color,
                Ok(Color::Hsbk(Some(240), None, Some(0.3), Some(3500)))
            );
            let color = "hue:240 saturation:0.531 kelvin:3500".parse();
            assert_eq!(
                color,
                Ok(Color::Hsbk(Some(240), Some(0.531), None, Some(3500)))
            );
            let color = "hue:240 saturation:0.531 brightness:0.3".parse();
            assert_eq!(
                color,
                Ok(Color::Hsbk(Some(240), Some(0.531), Some(0.3), None))
            );
            let color = "rgb:0,17,36".parse();
            assert_eq!(color, Ok(Color::Rgb([0, 17, 36])));
            let color = "#123456".parse();
            assert_eq!(color, Ok(Color::RgbStr("#123456".to_string())));
            let color = "#000000".parse();
            assert_eq!(color, Ok(Color::RgbStr("#000000".to_string())));
        }
        #[test]
        fn validate() {
            let color = Color::Red;
            assert!(color.validate().is_ok());
            let color = Color::Orange;
            assert!(color.validate().is_ok());
            let color = Color::Yellow;
            assert!(color.validate().is_ok());
            let color = Color::Green;
            assert!(color.validate().is_ok());
            let color = Color::Blue;
            assert!(color.validate().is_ok());
            let color = Color::Purple;
            assert!(color.validate().is_ok());
            let color = Color::White;
            assert!(color.validate().is_ok());
            let color = Color::Hue(370);
            assert_eq!(color.validate(), Err(Error::Hue(370)));
            let color = Color::Hue(300);
            assert!(color.validate().is_ok());
            let color = Color::Hue(0);
            assert!(color.validate().is_ok());
            let color = Color::Hue(0);
            assert!(color.validate().is_ok());
            let color = Color::Saturation(-1.0);
            assert_eq!(color.validate(), Err(Error::SaturationLow(-1.0)));
            let color = Color::Saturation(0.0);
            assert!(color.validate().is_ok());
            let color = Color::Saturation(1.0);
            assert!(color.validate().is_ok());
            let color = Color::Saturation(2.0);
            assert_eq!(color.validate(), Err(Error::SaturationHigh(2.0)));
            let color = Color::Brightness(-0.3);
            assert_eq!(color.validate(), Err(Error::BrightnessLow(-0.3)));
            let color = Color::Brightness(0.0);
            assert!(color.validate().is_ok());
            let color = Color::Brightness(3.4);
            assert_eq!(color.validate(), Err(Error::BrightnessHigh(3.4)));
            let color = Color::Kelvin(1000);
            assert_eq!(color.validate(), Err(Error::KelvinLow(1000)));
            let color = Color::Kelvin(1500);
            assert!(color.validate().is_ok());
            let color = Color::Kelvin(9000);
            assert!(color.validate().is_ok());
            let color = Color::Kelvin(9001);
            assert_eq!(color.validate(), Err(Error::KelvinHigh(9001)));
            let color = Color::Hsbk(Some(100), Some(0.1), Some(0.4), Some(2000));
            assert!(color.validate().is_ok());
            let color = Color::Hsbk(Some(100), None, None, None);
            assert!(color.validate().is_ok());
            let color = Color::Hsbk(None, Some(0.1), None, None);
            assert!(color.validate().is_ok());
            let color = Color::Hsbk(None, None, Some(0.4), None);
            assert!(color.validate().is_ok());
            let color = Color::Hsbk(None, None, None, Some(2000));
            assert!(color.validate().is_ok());
            let color = Color::Hsbk(Some(361), Some(0.1), Some(0.4), Some(2000));
            assert_eq!(color.validate(), Err(Error::Hue(361)));
            let color = Color::Hsbk(Some(360), Some(1.1), Some(0.4), Some(2000));
            assert_eq!(color.validate(), Err(Error::SaturationHigh(1.1)));
            let color = Color::Hsbk(Some(360), Some(-0.1), Some(0.4), Some(2000));
            assert_eq!(color.validate(), Err(Error::SaturationLow(-0.1)));
            let color = Color::Hsbk(Some(100), Some(0.1), Some(1.1), Some(2000));
            assert_eq!(color.validate(), Err(Error::BrightnessHigh(1.1)));
            let color = Color::Hsbk(Some(100), Some(0.1), Some(-0.1), Some(2000));
            assert_eq!(color.validate(), Err(Error::BrightnessLow(-0.1)));
            let color = Color::Hsbk(Some(100), None, None, Some(1499));
            assert_eq!(color.validate(), Err(Error::KelvinLow(1499)));
            let color = Color::Hsbk(Some(100), None, None, Some(10_000));
            assert_eq!(color.validate(), Err(Error::KelvinHigh(10_000)));
            let color = Color::RgbStr("#12345".to_string());
            assert_eq!(
                color.validate(),
                Err(Error::RgbStrShort(true, "#12345".to_string()))
            );
            let color = Color::RgbStr("12345".to_string());
            assert_eq!(
                color.validate(),
                Err(Error::RgbStrShort(false, "12345".to_string()))
            );
            let color = Color::RgbStr("123456".to_string());
            assert!(color.validate().is_ok());
            let color = Color::RgbStr("#123456".to_string());
            assert!(color.validate().is_ok());
            let color = Color::RgbStr("1234567".to_string());
            assert_eq!(
                color.validate(),
                Err(Error::RgbStrLong(false, "1234567".to_string()))
            );
            let color = Color::RgbStr("#1234567".to_string());
            assert_eq!(
                color.validate(),
                Err(Error::RgbStrLong(true, "#1234567".to_string()))
            );
        }
    }
}