use serde::Deserialize;
use crate::error::ParseError;
use crate::types::{ColorTemperature, HsbColor, PowerState};
#[derive(Debug, Clone, Deserialize)]
pub struct HsbColorResponse {
#[serde(rename = "HSBColor")]
hsb_color: String,
#[serde(rename = "Dimmer", default)]
dimmer: Option<u8>,
#[serde(rename = "POWER", default)]
power: Option<String>,
}
impl HsbColorResponse {
fn parse_hsb(&self) -> Result<(u16, u8, u8), ParseError> {
let parts: Vec<&str> = self.hsb_color.split(',').collect();
if parts.len() != 3 {
return Err(ParseError::InvalidValue {
field: "HSBColor".to_string(),
message: format!("expected 3 comma-separated values, got: {}", self.hsb_color),
});
}
let hue = parts[0]
.parse::<u16>()
.map_err(|_| ParseError::InvalidValue {
field: "HSBColor.hue".to_string(),
message: format!("invalid hue value: {}", parts[0]),
})?;
let saturation = parts[1]
.parse::<u8>()
.map_err(|_| ParseError::InvalidValue {
field: "HSBColor.saturation".to_string(),
message: format!("invalid saturation value: {}", parts[1]),
})?;
let brightness = parts[2]
.parse::<u8>()
.map_err(|_| ParseError::InvalidValue {
field: "HSBColor.brightness".to_string(),
message: format!("invalid brightness value: {}", parts[2]),
})?;
Ok((hue, saturation, brightness))
}
pub fn hue(&self) -> Result<u16, ParseError> {
self.parse_hsb().map(|(h, _, _)| h)
}
pub fn saturation(&self) -> Result<u8, ParseError> {
self.parse_hsb().map(|(_, s, _)| s)
}
pub fn brightness(&self) -> Result<u8, ParseError> {
self.parse_hsb().map(|(_, _, b)| b)
}
pub fn as_tuple(&self) -> Result<(u16, u8, u8), ParseError> {
self.parse_hsb()
}
pub fn hsb_color(&self) -> Result<HsbColor, ParseError> {
let (hue, saturation, brightness) = self.parse_hsb()?;
HsbColor::new(hue, saturation, brightness).map_err(|e| ParseError::InvalidValue {
field: "HSBColor".to_string(),
message: e.to_string(),
})
}
#[must_use]
pub fn raw(&self) -> &str {
&self.hsb_color
}
#[must_use]
pub fn dimmer(&self) -> Option<u8> {
self.dimmer
}
pub fn power_state(&self) -> Result<Option<PowerState>, ParseError> {
match &self.power {
Some(s) => s
.parse::<PowerState>()
.map(Some)
.map_err(|_| ParseError::InvalidValue {
field: "POWER".to_string(),
message: format!("invalid power state: {s}"),
}),
None => Ok(None),
}
}
#[must_use]
pub fn is_on(&self) -> Option<bool> {
self.power.as_ref().map(|s| s == "ON")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ColorTemperatureResponse {
#[serde(rename = "CT")]
ct: u16,
#[serde(rename = "POWER", default)]
power: Option<String>,
}
impl ColorTemperatureResponse {
#[must_use]
pub fn color_temperature(&self) -> u16 {
self.ct
}
#[must_use]
pub fn to_kelvin(&self) -> u32 {
if self.ct == 0 {
0
} else {
1_000_000 / u32::from(self.ct)
}
}
pub fn power_state(&self) -> Result<Option<PowerState>, ParseError> {
match &self.power {
Some(s) => s
.parse::<PowerState>()
.map(Some)
.map_err(|_| ParseError::InvalidValue {
field: "POWER".to_string(),
message: format!("invalid power state: {s}"),
}),
None => Ok(None),
}
}
#[must_use]
pub fn is_on(&self) -> Option<bool> {
self.power.as_ref().map(|s| s == "ON")
}
pub fn to_color_temperature(&self) -> Result<ColorTemperature, ParseError> {
ColorTemperature::new(self.ct).map_err(|e| ParseError::InvalidValue {
field: "CT".to_string(),
message: e.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hsb_color_only() {
let json = r#"{"HSBColor": "180,100,75"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.hue().unwrap(), 180);
assert_eq!(response.saturation().unwrap(), 100);
assert_eq!(response.brightness().unwrap(), 75);
assert_eq!(response.as_tuple().unwrap(), (180, 100, 75));
assert!(response.power_state().unwrap().is_none());
}
#[test]
fn parse_hsb_color_with_power() {
let json = r#"{"HSBColor": "0,100,100", "POWER": "ON"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.hue().unwrap(), 0);
assert_eq!(response.power_state().unwrap().unwrap(), PowerState::On);
assert_eq!(response.is_on(), Some(true));
}
#[test]
fn parse_hsb_color_with_dimmer() {
let json = r#"{"HSBColor": "120,80,50", "Dimmer": 50}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.hue().unwrap(), 120);
assert_eq!(response.dimmer(), Some(50));
}
#[test]
fn parse_hsb_color_full_response() {
let json = r#"{
"HSBColor": "240,50,75",
"Dimmer": 75,
"POWER": "ON",
"Color": "5959BF",
"White": 0,
"CT": 327
}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.as_tuple().unwrap(), (240, 50, 75));
assert_eq!(response.dimmer(), Some(75));
assert_eq!(response.power_state().unwrap().unwrap(), PowerState::On);
}
#[test]
fn parse_hsb_color_red() {
let json = r#"{"HSBColor": "0,100,100"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.as_tuple().unwrap(), (0, 100, 100));
}
#[test]
fn parse_hsb_color_green() {
let json = r#"{"HSBColor": "120,100,100"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.as_tuple().unwrap(), (120, 100, 100));
}
#[test]
fn parse_hsb_color_blue() {
let json = r#"{"HSBColor": "240,100,100"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.as_tuple().unwrap(), (240, 100, 100));
}
#[test]
fn parse_hsb_color_max_hue() {
let json = r#"{"HSBColor": "360,100,100"}"#;
let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.hue().unwrap(), 360);
}
#[test]
fn parse_hsb_invalid_format() {
let json = r#"{"HSBColor": "180,100"}"#; let response: HsbColorResponse = serde_json::from_str(json).unwrap();
assert!(response.as_tuple().is_err());
}
#[test]
fn parse_ct_only() {
let json = r#"{"CT": 326}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 326);
assert!(response.power_state().unwrap().is_none());
}
#[test]
fn parse_ct_with_power() {
let json = r#"{"CT": 250, "POWER": "ON"}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 250);
assert_eq!(response.power_state().unwrap().unwrap(), PowerState::On);
}
#[test]
fn parse_ct_coldest() {
let json = r#"{"CT": 153}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 153);
assert_eq!(response.to_kelvin(), 6535); }
#[test]
fn parse_ct_warmest() {
let json = r#"{"CT": 500}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 500);
assert_eq!(response.to_kelvin(), 2000); }
#[test]
fn parse_ct_neutral() {
let json = r#"{"CT": 326}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 326);
assert_eq!(response.to_kelvin(), 3067); }
#[test]
fn parse_ct_with_additional_fields() {
let json = r#"{
"CT": 327,
"POWER": "ON",
"Dimmer": 100,
"Color": "FFFFFF"
}"#;
let response: ColorTemperatureResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.color_temperature(), 327);
assert_eq!(response.power_state().unwrap().unwrap(), PowerState::On);
}
}