diff --git a/Cargo.lock b/Cargo.lock index 1ca62fc1..b99536b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,7 @@ dependencies = [ "hex", "iana-time-zone", "mac_address", + "maplit", "packed_struct", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index aad128bd..ce701bf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,14 @@ members = [ ] [workspace.lints.rust] +# NOTE: to use llvm-cov, comment out the "unstable_features" restriction: unstable_features = "forbid" unused_lifetimes = "warn" unused_qualifications = "warn" +# Needed for llvm-cov +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] } + [workspace.lints.clippy] all = { level = "warn", priority = -1 } correctness = { level = "warn", priority = -1 } diff --git a/crates/hue/Cargo.toml b/crates/hue/Cargo.toml index c2f72b1f..53e90b78 100644 --- a/crates/hue/Cargo.toml +++ b/crates/hue/Cargo.toml @@ -29,6 +29,7 @@ thiserror = "2.0.11" uuid = { version = "1.13.1", features = ["serde", "v5"] } mac_address = { version = "1.1.8", features = ["serde"], optional = true } +maplit = "1.0.2" [features] default = ["event", "mac", "rng"] diff --git a/crates/hue/src/clamp.rs b/crates/hue/src/clamp.rs index 5ae1c154..752ac0db 100644 --- a/crates/hue/src/clamp.rs +++ b/crates/hue/src/clamp.rs @@ -7,12 +7,12 @@ pub trait Clamp { impl Clamp for f32 { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn unit_to_u8_clamped(self) -> u8 { - (self * 255.0).clamp(0.0, 255.0) as u8 + (self * 255.0).round().clamp(0.0, 255.0) as u8 } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn unit_to_u8_clamped_light(self) -> u8 { - self.mul_add(253.0, 1.0).clamp(1.0, 254.0) as u8 + self.mul_add(253.0, 1.0).round().clamp(1.0, 254.0) as u8 } fn unit_from_u8(value: u8) -> Self { @@ -23,15 +23,75 @@ impl Clamp for f32 { impl Clamp for f64 { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn unit_to_u8_clamped(self) -> u8 { - (self * 255.0).clamp(0.0, 255.0) as u8 + (self * 255.0).round().clamp(0.0, 255.0) as u8 } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn unit_to_u8_clamped_light(self) -> u8 { - self.mul_add(253.0, 1.0).clamp(1.0, 254.0) as u8 + self.mul_add(253.0, 1.0).round().clamp(1.0, 254.0) as u8 } fn unit_from_u8(value: u8) -> Self { Self::from(value) / 255.0 } } + +#[cfg(test)] +mod tests { + use crate::clamp::Clamp; + use crate::{compare, compare_float}; + + #[test] + fn f32_unit_to_u8_clamped() { + assert_eq!((-1.0f32).unit_to_u8_clamped(), 0x00); + assert_eq!(0.0f32.unit_to_u8_clamped(), 0x00); + assert_eq!(0.5f32.unit_to_u8_clamped(), 0x80); + assert_eq!(1.0f32.unit_to_u8_clamped(), 0xFF); + assert_eq!(2.0f32.unit_to_u8_clamped(), 0xFF); + } + + #[test] + fn f64_unit_to_u8_clamped() { + assert_eq!((-1.0f64).unit_to_u8_clamped(), 0x00); + assert_eq!(0.0f64.unit_to_u8_clamped(), 0x00); + assert_eq!(0.5f64.unit_to_u8_clamped(), 0x80); + assert_eq!(1.0f64.unit_to_u8_clamped(), 0xFF); + assert_eq!(2.0f64.unit_to_u8_clamped(), 0xFF); + } + + #[test] + fn f32_unit_to_u8_clamped_light() { + assert_eq!((-1.0f32).unit_to_u8_clamped_light(), 0x01); + assert_eq!(0.0f32.unit_to_u8_clamped_light(), 0x01); + assert_eq!(0.5f32.unit_to_u8_clamped_light(), 0x80); + assert_eq!(1.0f32.unit_to_u8_clamped_light(), 0xFE); + assert_eq!(2.0f32.unit_to_u8_clamped_light(), 0xFE); + } + + #[test] + fn f64_unit_to_u8_clamped_light() { + assert_eq!((-1.0f64).unit_to_u8_clamped_light(), 0x01); + assert_eq!(0.0f64.unit_to_u8_clamped_light(), 0x01); + assert_eq!(0.5f64.unit_to_u8_clamped_light(), 0x80); + assert_eq!(1.0f64.unit_to_u8_clamped_light(), 0xFE); + assert_eq!(2.0f64.unit_to_u8_clamped_light(), 0xFE); + } + + #[test] + fn f32_unit_from_u8() { + compare!(f32::unit_from_u8(0x00), 0.0 / 255.0); + compare!(f32::unit_from_u8(0x01), 1.0 / 255.0); + compare!(f32::unit_from_u8(0x02), 2.0 / 255.0); + compare!(f32::unit_from_u8(0xFE), 254.0 / 255.0); + compare!(f32::unit_from_u8(0xFF), 255.0 / 255.0); + } + + #[test] + fn f64_unit_from_u8() { + compare!(f64::unit_from_u8(0x00), 0.0 / 255.0); + compare!(f64::unit_from_u8(0x01), 1.0 / 255.0); + compare!(f64::unit_from_u8(0x02), 2.0 / 255.0); + compare!(f64::unit_from_u8(0xFE), 254.0 / 255.0); + compare!(f64::unit_from_u8(0xFF), 255.0 / 255.0); + } +} diff --git a/crates/hue/src/colorspace.rs b/crates/hue/src/colorspace.rs index 785103b6..3a40039c 100644 --- a/crates/hue/src/colorspace.rs +++ b/crates/hue/src/colorspace.rs @@ -52,7 +52,7 @@ impl Matrix3 { } // Divide the row by the diagonal term - let inv = 1.0 / d; + let inv = d.recip(); for c in 0..3 { current[[i, c]] *= inv; inverse[[i, c]] *= inv; @@ -215,16 +215,8 @@ pub const ADOBE: ColorSpace = ColorSpace { mod tests { use std::iter::zip; - use crate::colorspace::{ADOBE, ColorSpace, SRGB, WIDE}; - - macro_rules! compare { - ($expr:expr, $value:expr) => { - let a = $expr; - let b = $value; - eprintln!("{a} vs {b:.4}"); - assert!((a - b).abs() < 1e-4); - }; - } + use crate::colorspace::{ADOBE, ColorSpace, Matrix3, SRGB, WIDE}; + use crate::{compare, compare_float, compare_matrix}; fn verify_matrix(cs: &ColorSpace) { let xyz = &cs.xyz; @@ -233,13 +225,8 @@ mod tests { let xyzi = xyz.inverted().unwrap(); let rgbi = rgb.inverted().unwrap(); - zip(xyz.0, rgbi.0).for_each(|(a, b)| { - compare!(a, b); - }); - - zip(rgb.0, xyzi.0).for_each(|(a, b)| { - compare!(a, b); - }); + compare_matrix!(xyz.0, rgbi.0); + compare_matrix!(rgb.0, xyzi.0); } #[test] @@ -256,4 +243,17 @@ mod tests { fn iverse_adobe() { verify_matrix(&ADOBE); } + + #[test] + fn invert_identity() { + let ident = Matrix3::identity(); + let inv = ident.inverted().unwrap(); + compare_matrix!(ident.0, inv.0); + } + + #[test] + fn invert_zero() { + let zero = Matrix3([0.0; 9]); + assert!(zero.inverted().is_none()); + } } diff --git a/crates/hue/src/colortemp.rs b/crates/hue/src/colortemp.rs index d1c8db8d..7c8394ce 100644 --- a/crates/hue/src/colortemp.rs +++ b/crates/hue/src/colortemp.rs @@ -51,24 +51,7 @@ pub fn cct_to_xy(cct: f64) -> XY { mod tests { use crate::colortemp::cct_to_xy; use crate::xy::XY; - - macro_rules! compare { - ($expr:expr, $value:expr) => { - let a = $expr; - let b = $value; - eprintln!("{a} vs {b:.4}"); - assert!((a - b).abs() < 1e-4); - }; - } - - macro_rules! compare_xy { - ($expr:expr, $value:expr) => { - let a = $expr; - let b = $value; - compare!(a.x, b.x); - compare!(a.y, b.y); - }; - } + use crate::{compare, compare_float, compare_xy}; // Regression tests, sanity checked against kelvin-to-blackbody raditation color // data found here: diff --git a/crates/hue/src/date_format.rs b/crates/hue/src/date_format.rs index 5db5307f..3aa564ab 100644 --- a/crates/hue/src/date_format.rs +++ b/crates/hue/src/date_format.rs @@ -158,37 +158,167 @@ pub mod legacy_utc_opt { date_deserializer_utc_opt!(DateTime, super::FORMAT_LOCAL); } +#[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { - use chrono::{DateTime, TimeZone, Utc}; + use std::fmt::Debug; + + use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + use serde_json::de::StrRead; use crate::error::HueResult; - fn date() -> (&'static str, DateTime) { + fn de( + ds: &'static str, + d1: &T, + desi: impl Fn(&mut serde_json::Deserializer) -> serde_json::Result, + ) -> HueResult<()> { + let mut deser = serde_json::Deserializer::from_str(ds); + let d2 = desi(&mut deser)?; + + assert_eq!(*d1, d2); + Ok(()) + } + + fn se( + s1: &'static str, + seri: impl Fn(&mut serde_json::Serializer<&mut Vec>) -> serde_json::Result<()>, + ) -> HueResult<()> { + let mut s2 = vec![]; + let mut ser = serde_json::Serializer::new(&mut s2); + seri(&mut ser)?; + + eprintln!("{} vs {}", s1, s2.escape_ascii()); + assert_eq!(s1.as_bytes(), s2); + Ok(()) + } + + fn date_utc() -> (&'static str, DateTime) { let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); ("\"2014-07-08T09:10:11Z\"", dt) } #[test] fn utc_de() -> HueResult<()> { - let (ds, d1) = date(); + let (ds, d1) = date_utc(); + de(ds, &d1, |de| super::utc::deserialize(de)) + } - let mut deser = serde_json::Deserializer::from_str(ds); - let d2 = super::utc::deserialize(&mut deser)?; + #[test] + fn utc_se() -> HueResult<()> { + let (s1, dt) = date_utc(); + se(s1, |ser| super::utc::serialize(&dt, ser)) + } - assert_eq!(d1, d2); - Ok(()) + fn date_utc_ms() -> (&'static str, DateTime) { + let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); + let dt = Utc + .timestamp_millis_opt(dt.timestamp_millis() + 123) + .unwrap(); + ("\"2014-07-08T09:10:11.123Z\"", dt) } #[test] - fn utc_se() -> HueResult<()> { - let (s1, dt) = date(); + fn utc_ms_de() -> HueResult<()> { + let (ds, d1) = date_utc_ms(); + de(ds, &d1, |de| super::utc_ms::deserialize(de)) + } - let mut s2 = vec![]; - let mut ser = serde_json::Serializer::new(&mut s2); - super::utc::serialize(&dt, &mut ser)?; + #[test] + fn utc_ms_se() -> HueResult<()> { + let (s1, dt) = date_utc_ms(); + se(s1, |ser| super::utc_ms::serialize(&dt, ser)) + } - assert_eq!(s1.as_bytes(), s2); - Ok(()) + #[test] + fn utc_ms_opt_de_some() -> HueResult<()> { + let (ds, d1) = date_utc_ms(); + de(ds, &Some(d1), |de| super::utc_ms_opt::deserialize(de)) + } + + #[test] + fn utc_ms_opt_de_none() -> HueResult<()> { + de("null", &None, |de| super::utc_ms_opt::deserialize(de)) + } + + #[test] + fn utc_ms_opt_se_some() -> HueResult<()> { + let (s1, dt) = date_utc_ms(); + se(s1, |ser| super::utc_ms_opt::serialize(&Some(dt), ser)) + } + + #[test] + fn utc_ms_opt_se_none() -> HueResult<()> { + se("null", |ser| super::utc_ms_opt::serialize(&None, ser)) + } + + fn date_legacy_naive() -> (&'static str, NaiveDateTime) { + let dt = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2014, 7, 8).unwrap(), + NaiveTime::from_hms_opt(9, 10, 11).unwrap(), + ); + ("\"2014-07-08T09:10:11\"", dt) + } + + #[test] + fn legacy_naive_de() -> HueResult<()> { + let (ds, d1) = date_legacy_naive(); + de(ds, &d1, |de| super::legacy_naive::deserialize(de)) + } + + #[test] + fn legacy_naive_se() -> HueResult<()> { + let (s1, dt) = date_legacy_naive(); + se(s1, |ser| super::legacy_naive::serialize(&dt, ser)) + } + + fn date_legacy_local_opt() -> (&'static str, DateTime) { + let dt = Local.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); + ("\"2014-07-08T09:10:11\"", dt) + } + + #[test] + fn legacy_local_opt_de_some() -> HueResult<()> { + let (ds, d1) = date_legacy_local_opt(); + de(ds, &Some(d1), |de| super::legacy_local_opt::deserialize(de)) + } + + #[test] + fn legacy_local_opt_se_some() -> HueResult<()> { + let (s1, dt) = date_legacy_local_opt(); + se(s1, |ser| super::legacy_local_opt::serialize(&Some(dt), ser)) + } + + #[test] + fn legacy_local_opt_de_none() -> HueResult<()> { + de("null", &None, |de| super::legacy_local_opt::deserialize(de)) + } + + #[test] + fn legacy_local_opt_se_none() -> HueResult<()> { + se("null", |ser| super::legacy_local_opt::serialize(&None, ser)) + } + + #[test] + fn update_utc_de() -> HueResult<()> { + let (ds, d1) = date_utc(); + de(ds, &d1, |de| super::update_utc::deserialize(de)) + } + + fn date_legacy_utc() -> (&'static str, DateTime) { + let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); + ("\"2014-07-08T09:10:11\"", dt) + } + + #[test] + fn legacy_utc_de() -> HueResult<()> { + let (ds, d1) = date_legacy_utc(); + de(ds, &d1, |de| super::legacy_utc::deserialize(de)) + } + + #[test] + fn legacy_utc_se() -> HueResult<()> { + let (s1, dt) = date_legacy_utc(); + se(s1, |ser| super::legacy_utc::serialize(&dt, ser)) } } diff --git a/crates/hue/src/devicedb.rs b/crates/hue/src/devicedb.rs index df537143..ed32e27c 100644 --- a/crates/hue/src/devicedb.rs +++ b/crates/hue/src/devicedb.rs @@ -1,3 +1,6 @@ +use std::collections::BTreeMap; +use std::sync::LazyLock; + use crate::api::{DeviceArchetype, DeviceProductData}; // This file contains discovered product data from multiple sources, @@ -10,7 +13,7 @@ use crate::api::{DeviceArchetype, DeviceProductData}; // provide more realistic API data, even when certain information is not // available from the backend (zigbee2mqtt). -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SimpleProductData<'a> { pub manufacturer_name: &'a str, pub product_name: &'a str, @@ -35,15 +38,16 @@ impl<'a> SimpleProductData<'a> { } } -// use shorter alias for better formatting -#[allow(clippy::enum_glob_use)] -use DeviceArchetype::*; -use SimpleProductData as SPD; +static PRODUCT_DATA: LazyLock> = LazyLock::new(make_product_data); -#[allow(clippy::match_same_arms)] -#[must_use] -pub fn product_data(model_id: &str) -> Option> { - let pd = match model_id { +#[cfg_attr(coverage_nightly, coverage(off))] +fn make_product_data() -> BTreeMap<&'static str, SimpleProductData<'static>> { + // use shorter alias for better formatting + #[allow(clippy::enum_glob_use)] + use DeviceArchetype::*; + use SimpleProductData as SPD; + + maplit::btreemap! { "915005987201" => SPD::signify("Signe gradient floor", HueSigne, "100b-118"), "929003053301_01" => SPD::signify("Hue Ensis up", PendantLong, "100b-11f"), "929003053301_02" => SPD::signify("Hue Ensis down", PendantLong, "100b-11f"), @@ -77,9 +81,12 @@ pub fn product_data(model_id: &str) -> Option> { product_archetype: UnknownArchetype, hardware_platform_type: Some("1144-0"), }, - _ => return None, - }; - Some(pd) + } +} + +#[must_use] +pub fn product_data(model_id: &str) -> Option> { + PRODUCT_DATA.get(model_id).cloned() } #[must_use] @@ -91,3 +98,27 @@ pub fn product_archetype(model_id: &str) -> Option { pub fn hardware_platform_type(model_id: &str) -> Option<&'static str> { product_data(model_id).and_then(|pd| pd.hardware_platform_type) } + +#[cfg(test)] +mod tests { + use crate::api::DeviceArchetype; + use crate::devicedb::{hardware_platform_type, product_archetype, product_data}; + + #[test] + fn lookup_spf() { + assert!(product_data("LCX001").is_some()); + } + + #[test] + fn lookup_archetype() { + assert_eq!( + product_archetype("LCX001").unwrap(), + DeviceArchetype::HueLightstripTv + ); + } + + #[test] + fn lookup_platform_type() { + assert_eq!(hardware_platform_type("LCX001").unwrap(), "100b-118",); + } +} diff --git a/crates/hue/src/diff.rs b/crates/hue/src/diff.rs index a257f454..2f507729 100644 --- a/crates/hue/src/diff.rs +++ b/crates/hue/src/diff.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use serde::{Serialize, de::DeserializeOwned}; use serde_json::{Map, Value}; use crate::error::{HueError, HueResult}; @@ -54,14 +55,32 @@ pub fn event_update_diff(ma: Value, mb: Value) -> HueResult> { Ok(Some(Value::Object(diff))) } +pub fn event_update_apply(ma: &T, mb: Value) -> HueResult { + let ma = serde_json::to_value(ma)?; + + let (Value::Object(mut a), Value::Object(b)) = (ma, mb) else { + return Err(HueError::Unmergable); + }; + + for (key, value) in b { + a.insert(key, value); + } + + Ok(serde_json::from_value(Value::Object(a))?) +} + +#[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { + use serde_json::Value; use serde_json::json; + use crate::diff::event_update_apply as apply; use crate::diff::event_update_diff as diff; + use crate::error::HueError; #[test] - fn test_diff_empty() { + fn diff_empty() { let a = json!({}); let b = json!({}); @@ -69,7 +88,15 @@ mod tests { } #[test] - fn test_diff_value_unchanged() { + fn diff_invalid() { + let a = json!([]); + let b = json!({}); + + assert!(matches!(diff(a, b).unwrap_err(), HueError::Undiffable)); + } + + #[test] + fn diff_value_unchanged() { let a = json!({"x": 42}); let b = json!({"x": 42}); @@ -77,7 +104,7 @@ mod tests { } #[test] - fn test_diff_whitelist_unchanged() { + fn diff_whitelist_unchanged() { let a = json!({"owner": 42}); let b = json!({"owner": 42}); @@ -85,7 +112,7 @@ mod tests { } #[test] - fn test_diff_value_removed() { + fn diff_value_removed() { let a = json!({"x": 42}); let b = json!({}); @@ -93,7 +120,7 @@ mod tests { } #[test] - fn test_diff_value_added() { + fn diff_value_added() { let a = json!({}); let b = json!({"x": 42}); let c = json!({"x": 42}); @@ -102,7 +129,7 @@ mod tests { } #[test] - fn test_diff_value_changed() { + fn diff_value_changed() { let a = json!({"x": 17}); let b = json!({"x": 42}); let c = json!({"x": 42}); @@ -111,7 +138,7 @@ mod tests { } #[test] - fn test_diff_whitelist_removed() { + fn diff_whitelist_removed() { let a = json!({"owner": 17}); let b = json!({}); let c = json!({"owner": 17}); @@ -120,7 +147,7 @@ mod tests { } #[test] - fn test_diff_whitelist_added() { + fn diff_whitelist_added() { let a = json!({}); let b = json!({"owner": 17}); let c = json!({"owner": 17}); @@ -129,7 +156,7 @@ mod tests { } #[test] - fn test_diff_whitelist_changed() { + fn diff_whitelist_changed() { let a = json!({"owner": 17}); let b = json!({"owner": 42}); let c = json!({"owner": 42}); @@ -138,7 +165,7 @@ mod tests { } #[test] - fn test_diff_value_type_changed() { + fn diff_value_type_changed() { let a = json!({"x": 17}); let b = json!({"x": "foo"}); let c = json!({"x": "foo"}); @@ -147,11 +174,69 @@ mod tests { } #[test] - fn test_diff_whitelist_type_changed() { + fn diff_whitelist_type_changed() { let a = json!({"owner": 17}); let b = json!({"owner": "foo"}); let c = json!({"owner": "foo"}); assert_eq!(diff(a, b).unwrap(), Some(c)); } + + #[test] + fn apply_empty() { + let a = json!({}); + let b = json!({}); + let c = json!({}); + + assert_eq!(apply(&a, b).unwrap(), c); + } + + #[test] + fn apply_invalid() { + let a = json!({}); + let b = json!([]); + + assert!(matches!(apply(&a, b).unwrap_err(), HueError::Unmergable)); + + let a = json!([]); + let b = json!({}); + + assert!(matches!(apply(&a, b).unwrap_err(), HueError::Unmergable)); + } + + #[test] + fn apply_simply() { + let a = json!({}); + let b = json!({"x": "y"}); + let c = json!({"x": "y"}); + + assert_eq!(apply(&a, b).unwrap(), c); + } + + #[test] + fn apply_overwrite() { + let a = json!({"x": "before"}); + let b = json!({"x": "after"}); + let c = json!({"x": "after"}); + + assert_eq!(apply(&a, b).unwrap(), c); + } + + #[test] + fn apply_null() { + let a = json!({"x": "before"}); + let b = json!({"x": Value::Null}); + let c = json!({"x": Value::Null}); + + assert_eq!(apply(&a, b).unwrap(), c); + } + + #[test] + fn apply_some() { + let a = json!({"x": "unchanged"}); + let b = json!({"x": "unchanged", "y": "new"}); + let c = json!({"x": "unchanged", "y": "new"}); + + assert_eq!(apply(&a, b).unwrap(), c); + } } diff --git a/crates/hue/src/error.rs b/crates/hue/src/error.rs index 1d7e6bc1..29ce322a 100644 --- a/crates/hue/src/error.rs +++ b/crates/hue/src/error.rs @@ -52,6 +52,9 @@ pub enum HueError { #[error("Cannot generate json difference between non-map objects")] Undiffable, + + #[error("Cannot merge json difference between non-map object")] + Unmergable, } /// Error types for Hue Bridge v1 API @@ -103,6 +106,7 @@ pub enum HueApiV1Error { } impl HueApiV1Error { + #[cfg_attr(coverage_nightly, coverage(off))] #[must_use] pub const fn error_code(&self) -> u32 { *self as u32 diff --git a/crates/hue/src/event.rs b/crates/hue/src/event.rs index 0a4047c6..88931f2d 100644 --- a/crates/hue/src/event.rs +++ b/crates/hue/src/event.rs @@ -10,8 +10,6 @@ use crate::date_format; use crate::api::ResourceLink; #[cfg(feature = "rng")] use crate::error::HueResult; -#[cfg(feature = "rng")] -use serde_json::json; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase", tag = "type")] @@ -57,16 +55,16 @@ impl EventBlock { }) } - pub fn delete(link: ResourceLink, id_v1: Option) -> HueResult { + pub fn delete(link: ResourceLink, id_v1: Option) -> HueResult { Ok(Self { creationtime: Utc::now(), id: Uuid::new_v4(), event: Event::Delete(Delete { - data: vec![json!({ - "id": link.rid, - "id_v1": id_v1, - "type": link.rtype, - })], + data: vec![ObjectDelete { + id: link.rid, + rtype: link.rtype, + id_v1, + }], }), }) } @@ -93,10 +91,89 @@ pub struct Update { pub data: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ObjectDelete { + pub id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub id_v1: Option, + #[serde(rename = "type")] + pub rtype: RType, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Delete { - pub data: Vec, + pub data: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Error {} + +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg(test)] +mod tests { + use serde_json::json; + use uuid::Uuid; + + use crate::api::{RType, Resource, ResourceLink, ResourceRecord}; + use crate::event::{Add, Delete, Event, EventBlock, Update}; + + // just some uuid for testing + const ID: Uuid = Uuid::NAMESPACE_DNS; + + #[test] + fn add() { + let obj = ResourceRecord::new( + ID, + None, + Resource::AuthV1(ResourceLink { + rid: ID, + rtype: RType::AuthV1, + }), + ); + + let add = EventBlock::add(vec![obj.clone()]); + let Event::Add(Add { data }) = add.event else { + panic!("Wrong event type"); + }; + + assert!(data.len() == 1); + + assert_eq!( + serde_json::to_string(&data[0]).unwrap(), + serde_json::to_string(&obj).unwrap() + ); + } + + #[test] + fn update() { + let diff = json!({"key": "value"}); + + let evt = EventBlock::update(&ID, Some("foo".into()), RType::AuthV1, diff.clone()).unwrap(); + let Event::Update(Update { data }) = evt.event else { + panic!("Wrong event type"); + }; + + assert!(data.len() == 1); + + let out = &data[0]; + assert_eq!(out.id_v1, Some("foo".into())); + assert_eq!(out.rtype, RType::AuthV1); + assert_eq!(out.data, diff); + } + + #[test] + fn delete() { + let evt = EventBlock::delete(RType::AuthV1.link_to(ID), Some("foo".into())).unwrap(); + + let Event::Delete(Delete { data }) = evt.event else { + panic!("Wrong event type"); + }; + + assert!(data.len() == 1); + + let out = &data[0]; + assert_eq!(out.id_v1, Some("foo".into())); + assert_eq!(out.rtype, RType::AuthV1); + assert_eq!(out.id, ID); + } +} diff --git a/crates/hue/src/flags.rs b/crates/hue/src/flags.rs index f55969c5..907fa58d 100644 --- a/crates/hue/src/flags.rs +++ b/crates/hue/src/flags.rs @@ -11,3 +11,36 @@ impl TakeFlag for T { found } } + +#[cfg(test)] +mod tests { + use bitflags::bitflags; + + use crate::flags::TakeFlag; + + bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct Flags: u16 { + const BIT = 1; + } + } + + #[test] + fn take_none() { + let mut fl = Flags::from_bits(0).unwrap(); + assert!(!fl.take(Flags::BIT)); + } + + #[test] + fn take_one() { + let mut fl = Flags::from_bits(1).unwrap(); + assert!(fl.take(Flags::BIT)); + } + + #[test] + fn take_twice() { + let mut fl = Flags::from_bits(1).unwrap(); + assert!(fl.take(Flags::BIT)); + assert!(!fl.take(Flags::BIT)); + } +} diff --git a/crates/hue/src/gamma.rs b/crates/hue/src/gamma.rs index 4d899bec..c2c0078a 100644 --- a/crates/hue/src/gamma.rs +++ b/crates/hue/src/gamma.rs @@ -63,11 +63,27 @@ impl GammaCorrection { #[cfg(test)] mod tests { use crate::gamma::GammaCorrection; + use crate::{compare, compare_float}; - macro_rules! compare { - ($expr:expr, $value:expr) => { - assert!(($expr - $value).abs() < 1e-5); - }; + #[test] + fn gamma_new() { + let gc = GammaCorrection::new(1.0, 2.0, 3.0, 4.0); + + compare!(gc.gamma, 1.0); + compare!(gc.transition, 2.0); + compare!(gc.slope, 3.0); + compare!(gc.offset, 4.0); + } + + #[test] + fn gamma_default() { + let gc = GammaCorrection::default(); + let none = GammaCorrection::NONE; + + compare!(gc.gamma, none.gamma); + compare!(gc.transition, none.transition); + compare!(gc.slope, none.slope); + compare!(gc.offset, none.offset); } #[test] diff --git a/crates/hue/src/hs.rs b/crates/hue/src/hs.rs index ca8457f3..b1d98cf2 100644 --- a/crates/hue/src/hs.rs +++ b/crates/hue/src/hs.rs @@ -24,22 +24,7 @@ impl From for HS { #[cfg(test)] mod tests { use crate::hs::{HS, RawHS}; - - macro_rules! compare { - ($expr:expr, $value:expr) => { - let a = $expr; - let b = $value; - eprintln!("{a} vs {b:.4}"); - assert!((a - b).abs() < 1e-4); - }; - } - - macro_rules! compare_hs { - ($a:expr, $b:expr) => {{ - compare!($a.hue, $b.hue); - compare!($a.sat, $b.sat); - }}; - } + use crate::{compare, compare_float, compare_hs}; #[test] fn from_rawhs_min() { diff --git a/crates/hue/src/legacy_api.rs b/crates/hue/src/legacy_api.rs index 19d9ec4e..dce7b632 100644 --- a/crates/hue/src/legacy_api.rs +++ b/crates/hue/src/legacy_api.rs @@ -906,3 +906,22 @@ impl Capabilities { } } } + +#[cfg(test)] +mod tests { + #[cfg(feature = "mac")] + #[test] + fn serialize_lower_case_mac() { + use mac_address::MacAddress; + + use crate::legacy_api::serialize_lower_case_mac; + + let mac = MacAddress::new([0x01, 0x02, 0x03, 0xAA, 0xBB, 0xCC]); + let mut res = vec![]; + let mut ser = serde_json::Serializer::new(&mut res); + + serialize_lower_case_mac(&mac, &mut ser).unwrap(); + + assert_eq!(res, b"\"01:02:03:aa:bb:cc\""); + } +} diff --git a/crates/hue/src/lib.rs b/crates/hue/src/lib.rs index 3c1f1dbb..87009a5a 100644 --- a/crates/hue/src/lib.rs +++ b/crates/hue/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../../../doc/hue-zigbee-format.md")] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] pub mod api; pub mod clamp; @@ -52,17 +53,114 @@ pub fn bridge_id(mac: MacAddress) -> String { #[cfg(test)] mod tests { + use mac_address::MacAddress; + use crate::version::SwVersion; use crate::{HUE_BRIDGE_V2_DEFAULT_APIVERSION, HUE_BRIDGE_V2_DEFAULT_SWVERSION}; + #[macro_export] + macro_rules! compare_float { + ($expr:expr, $value:expr, $diff:expr) => { + let a = $expr; + let b = $value; + eprintln!("{a} vs {b:.4} (diff {})", $diff); + assert!((a - b).abs() < $diff); + }; + } + + #[macro_export] + macro_rules! compare { + ($expr:expr, $value:expr) => { + compare_float!($expr, $value, 1e-4) + }; + } + + #[macro_export] + macro_rules! compare_hs { + ($a:expr, $b:expr) => {{ + compare!($a.hue, $b.hue); + compare!($a.sat, $b.sat); + }}; + } + + #[macro_export] + macro_rules! compare_xy { + ($expr:expr, $value:expr) => { + let a = $expr; + let b = $value; + compare!(a.x, b.x); + compare!(a.y, b.y); + }; + } + + #[macro_export] + macro_rules! compare_xy_quant { + ($expr:expr, $value:expr) => { + let a = $expr; + let b = $value; + compare_float!(a.x, b.x, 1e-3); + compare_float!(a.y, b.y, 1e-3); + }; + } + + #[macro_export] + macro_rules! compare_rgb { + ($a:expr, $b:expr) => {{ + eprintln!("Comparing r"); + compare!($a[0], $b[0]); + eprintln!("Comparing g"); + compare!($a[1], $b[1]); + eprintln!("Comparing b"); + compare!($a[2], $b[2]); + }}; + } + + #[macro_export] + macro_rules! compare_matrix { + ($a:expr, $b:expr) => { + zip($a, $b).for_each(|(a, b)| { + compare!(a, b); + }); + }; + } + + #[macro_export] + macro_rules! compare_hsl_rgb { + ($h:expr, $s:expr, $rgb:expr) => {{ + let sat = $s; + compare_rgb!(XY::rgb_from_hsl(HS { hue: $h, sat }, 0.5), $rgb); + }}; + } + /// verify that `HUE_BRIDGE_V2_DEFAULT_SWVERSION` and /// `HUE_BRIDGE_V2_DEFAULT_APIVERSION` are synchronized #[test] - fn test_default_version_match() { + fn default_version_match() { let ver = SwVersion::new(HUE_BRIDGE_V2_DEFAULT_SWVERSION, String::new()); assert_eq!( HUE_BRIDGE_V2_DEFAULT_APIVERSION, ver.get_legacy_apiversion() ); } + + #[test] + fn best_guess_timezone() { + let res = crate::best_guess_timezone(); + assert!(!res.is_empty()); + assert_ne!(res, "none"); + } + + #[test] + fn bridge_id() { + let mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let id = crate::bridge_id(mac); + assert_eq!(id, "112233fffe445566"); + } + + #[test] + fn bridge_id_raw() { + let mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + let id = crate::bridge_id_raw(mac); + assert_eq!(id, [0x11, 0x22, 0x33, 0xFF, 0xFE, 0x44, 0x55, 0x66]); + } } diff --git a/crates/hue/src/stream.rs b/crates/hue/src/stream.rs index 5e668f43..2fb58c43 100644 --- a/crates/hue/src/stream.rs +++ b/crates/hue/src/stream.rs @@ -101,14 +101,17 @@ impl HueStreamPacket { const ASCII_UUID_SIZE: usize = 36; pub fn parse(data: &[u8]) -> HueResult { - let (header, body) = data.split_at(HueStreamHeader::SIZE); - let hdr = HueStreamHeader::parse(header)?; + let hdr = HueStreamHeader::parse(data)?; + let body = &data[HueStreamHeader::SIZE..]; match hdr.version { HueStreamVersion::V1 => { let lights = HueStreamLightsV1::parse(hdr.color_mode, body)?; Ok(Self::V1(HueStreamPacketV1 { lights })) } HueStreamVersion::V2 => { + if body.len() < Self::ASCII_UUID_SIZE { + return Err(HueError::HueEntertainmentBadHeader); + } let (area_bytes, body) = body.split_at(Self::ASCII_UUID_SIZE); let area = Uuid::try_parse_ascii(area_bytes)?; let lights = HueStreamLightsV2::parse(hdr.color_mode, body)?; @@ -239,27 +242,16 @@ impl Xy16 { } } +#[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod tests { - use crate::{ - stream::{Rgb16, Xy16}, - xy::XY, + use crate::error::HueError; + use crate::stream::{ + HueStreamColorMode, HueStreamHeader, HueStreamLightsV1, HueStreamLightsV2, HueStreamPacket, + Rgb16, Xy16, }; - - macro_rules! compare_float { - ($expr:expr, $value:expr, $diff:expr) => { - let a = $expr; - let b = $value; - eprintln!("{a} vs {b:.4}"); - assert!((a - b).abs() < $diff); - }; - } - - macro_rules! compare { - ($expr:expr, $value:expr) => { - compare_float!($expr, $value, 1e-5) - }; - } + use crate::xy::XY; + use crate::{compare, compare_float, compare_xy}; #[test] fn rgb16_to_xy() { @@ -271,8 +263,7 @@ mod tests { let (xy, b) = rgb16.to_xy(); - compare!(xy.x, XY::D50_WHITE_POINT.x); - compare!(xy.y, XY::D50_WHITE_POINT.y); + compare_xy!(xy, XY::D50_WHITE_POINT); compare_float!(b, 255.0, 1e-2); } @@ -290,4 +281,203 @@ mod tests { compare!(xy.y, 1.0); compare!(b, 255.0); } + + #[test] + fn parse_stream_lights_v1_rgb() { + let data = [0x11, 0x22, 0x33, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]; + let raw = HueStreamLightsV1::parse(HueStreamColorMode::Rgb, &data).unwrap(); + let res = match raw { + HueStreamLightsV1::Rgb(rgb) => rgb, + HueStreamLightsV1::Xy(_) => panic!(), + }; + + assert_eq!(res.len(), 1); + assert_eq!(res[0].light_id, 0x11_22_33); + assert_eq!(res[0].rgb.r, 0xA0A1); + assert_eq!(res[0].rgb.g, 0xB0B1); + assert_eq!(res[0].rgb.b, 0xC0C1); + } + + #[test] + fn parse_stream_lights_v1_xy() { + let data = [0x11, 0x22, 0x33, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]; + let raw = HueStreamLightsV1::parse(HueStreamColorMode::Xy, &data).unwrap(); + let res = match raw { + HueStreamLightsV1::Rgb(_) => panic!(), + HueStreamLightsV1::Xy(xy) => xy, + }; + + assert_eq!(res.len(), 1); + assert_eq!(res[0].light_id, 0x11_22_33); + assert_eq!(res[0].xy.x, 0xA0A1); + assert_eq!(res[0].xy.y, 0xB0B1); + assert_eq!(res[0].xy.b, 0xC0C1); + } + + #[test] + fn parse_stream_lights_v2_rgb() { + let data = [0x11, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]; + let raw = HueStreamLightsV2::parse(HueStreamColorMode::Rgb, &data).unwrap(); + let res = match raw { + HueStreamLightsV2::Rgb(rgb) => rgb, + HueStreamLightsV2::Xy(_) => panic!(), + }; + + assert_eq!(res.len(), 1); + assert_eq!(res[0].channel, 0x11); + assert_eq!(res[0].rgb.r, 0xA0A1); + assert_eq!(res[0].rgb.g, 0xB0B1); + assert_eq!(res[0].rgb.b, 0xC0C1); + } + + #[test] + fn parse_stream_lights_v2_xy() { + let data = [0x11, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]; + let raw = HueStreamLightsV2::parse(HueStreamColorMode::Xy, &data).unwrap(); + let res = match raw { + HueStreamLightsV2::Rgb(_) => panic!(), + HueStreamLightsV2::Xy(xy) => xy, + }; + + assert_eq!(res.len(), 1); + assert_eq!(res[0].channel, 0x11); + assert_eq!(res[0].xy.x, 0xA0A1); + assert_eq!(res[0].xy.y, 0xB0B1); + assert_eq!(res[0].xy.b, 0xC0C1); + } + + #[test] + fn parse_packet_bad_size() { + let data = vec![0x00, 0x01]; + + let err = HueStreamPacket::parse(&data).unwrap_err(); + assert!(matches!(err, HueError::HueEntertainmentBadHeader)); + } + + #[test] + fn parse_packet_bad_header() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x01, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x00, // color_mode: rgb + 0x00, // x2, + ]); + + // corrupt first byte + data[0] = b'X'; + + let err = HueStreamPacket::parse(&data).unwrap_err(); + assert!(matches!(err, HueError::HueEntertainmentBadHeader)); + } + + #[test] + fn parse_packet_v1_rgb() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x01, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x00, // color_mode: rgb + 0x00, // x2, + ]); + data.extend_from_slice(&[0x11, 0x22, 0x33, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]); + + let res = HueStreamPacket::parse(&data).unwrap(); + + assert_eq!(res.color_mode(), HueStreamColorMode::Rgb); + + match res { + HueStreamPacket::V1(v1) => { + assert_eq!(v1.light_ids(), [0x11_22_33]); + } + HueStreamPacket::V2(_) => panic!(), + } + } + + #[test] + fn parse_packet_v1_xy() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x01, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x01, // color_mode: xy + 0x00, // x2, + ]); + data.extend_from_slice(&[0x11, 0x22, 0x33, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]); + + let res = HueStreamPacket::parse(&data).unwrap(); + + assert_eq!(res.color_mode(), HueStreamColorMode::Xy); + + match res { + HueStreamPacket::V1(v1) => { + assert_eq!(v1.light_ids(), [0x11_22_33]); + } + HueStreamPacket::V2(_) => panic!(), + } + } + + #[test] + fn parse_packet_v2_missing_uuid() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x02, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x00, // color_mode: rgb + 0x00, // x2, + ]); + + // dummy data + data.push(0x00); + + let err = HueStreamPacket::parse(&data).unwrap_err(); + + assert!(matches!(err, HueError::HueEntertainmentBadHeader)); + } + + #[test] + fn parse_packet_v2_rgb() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x02, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x00, // color_mode: rgb + 0x00, // x2, + ]); + data.extend_from_slice(b"01010101-0202-0303-0404-050505050505"); + data.extend_from_slice(&[0x11, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]); + + let res = HueStreamPacket::parse(&data).unwrap(); + + assert_eq!(res.color_mode(), HueStreamColorMode::Rgb); + } + + #[test] + fn parse_packet_v2_xy() { + let mut data = HueStreamHeader::MAGIC.to_vec(); + data.extend_from_slice(&[ + 0x02, // version + 0x00, // x0 + 0x00, // seqnr + 0x00, 0x00, // x1 + 0x01, // color_mode: xy + 0x00, // x2, + ]); + data.extend_from_slice(b"01010101-0202-0303-0404-050505050505"); + data.extend_from_slice(&[0x11, 0xA0, 0xA1, 0xB0, 0xB1, 0xC0, 0xC1]); + + let res = HueStreamPacket::parse(&data).unwrap(); + + assert_eq!(res.color_mode(), HueStreamColorMode::Xy); + } } diff --git a/crates/hue/src/update.rs b/crates/hue/src/update.rs index 679e61a7..d1920174 100644 --- a/crates/hue/src/update.rs +++ b/crates/hue/src/update.rs @@ -30,3 +30,16 @@ pub struct UpdateEntries { pub fn update_url_for_bridge(device_type_id: &str, version: u64) -> String { format!("{UPDATE_CHECK_URL}?deviceTypeId={device_type_id}&version={version}") } + +#[cfg(test)] +mod tests { + use crate::update::{UPDATE_CHECK_URL, update_url_for_bridge}; + + #[test] + fn url() { + assert_eq!( + update_url_for_bridge("dev", 1234), + format!("{UPDATE_CHECK_URL}?deviceTypeId=dev&version=1234") + ); + } +} diff --git a/crates/hue/src/version.rs b/crates/hue/src/version.rs index bea317de..86f86a8f 100644 --- a/crates/hue/src/version.rs +++ b/crates/hue/src/version.rs @@ -76,3 +76,73 @@ impl SwVersion { format!("{}.{}.{}", &version[0..1], &version[2..4], version) } } + +#[cfg(test)] +mod tests { + use crate::version::SwVersion; + use crate::{HUE_BRIDGE_V2_DEFAULT_APIVERSION, HUE_BRIDGE_V2_DEFAULT_SWVERSION}; + + #[allow(clippy::nonminimal_bool)] + #[test] + fn partial_ord() { + let a = SwVersion { + version: 10, + name: String::new(), + }; + let b = SwVersion { + version: 20, + name: String::new(), + }; + + assert!(a < b); + assert!(!(a >= b)); + } + + #[test] + fn default() { + let def = SwVersion::default(); + + assert_eq!( + def, + SwVersion { + version: HUE_BRIDGE_V2_DEFAULT_SWVERSION, + name: HUE_BRIDGE_V2_DEFAULT_APIVERSION.to_string(), + } + ); + } + + #[test] + fn debug() { + let version = SwVersion { + version: 1234, + name: "name".to_string(), + }; + assert_eq!(format!("{version:?}"), "name (1234)"); + } + + #[test] + fn as_u64() { + assert_eq!( + SwVersion::default().as_u64(), + HUE_BRIDGE_V2_DEFAULT_SWVERSION + ); + } + + #[test] + fn get_legacy_swversion() { + let version = SwVersion::new(1234, String::new()); + assert_eq!(version.get_legacy_swversion(), "1234"); + } + + #[test] + fn get_legacy_apiversion() { + let version = SwVersion::new(12345, String::new()); + assert_eq!(version.get_legacy_apiversion(), "1.34.0"); + } + + #[test] + fn get_software_version() { + let version = SwVersion::new(123_456, String::new()); + assert_eq!(version.get_software_version(), "1.34.123456"); + } +} diff --git a/crates/hue/src/xy.rs b/crates/hue/src/xy.rs index 8735f422..b48caf94 100644 --- a/crates/hue/src/xy.rs +++ b/crates/hue/src/xy.rs @@ -43,9 +43,9 @@ impl XY { let max_y = Self::COLOR_SPACE.find_maximum_y(x, y); if max_y > f64::EPSILON { - (Self { x, y }, b / max_y * 255.0) + (Self { x, y }, (b / max_y * 255.0).min(255.0)) } else { - (Self::D65_WHITE_POINT, 0.0) + (Self::D50_WHITE_POINT, 0.0) } } @@ -79,7 +79,7 @@ impl XY { } else if h < 5.0 { [m + x, m, m + c] } else { - [m + c, m + 0.0, m + x] + [m + c, m, m + x] } } @@ -106,8 +106,8 @@ impl XY { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[must_use] pub fn to_quant(&self) -> [u8; 3] { - let x = (self.x * ((f64::from(0xFFF) / WIDE_GAMUT_MAX_X) + (0.5 / 4095.))) as u16; - let y = (self.y * ((f64::from(0xFFF) / WIDE_GAMUT_MAX_Y) + (0.5 / 4095.))) as u16; + let x = ((self.x * f64::from(0xFFF)) / WIDE_GAMUT_MAX_X) as u16; + let y = ((self.y * f64::from(0xFFF)) / WIDE_GAMUT_MAX_Y) as u16; debug_assert!(x < 0x1000); debug_assert!(y < 0x1000); @@ -138,33 +138,10 @@ impl From for [f64; 2] { mod tests { use crate::hs::HS; use crate::xy::XY; - - macro_rules! compare { - ($expr:expr, $value:expr) => { - let a = $expr; - let b = $value; - eprintln!("{a} vs {b:.4}"); - assert!((a - b).abs() < 1e-4); - }; - } - - macro_rules! compare_rgb { - ($a:expr, $b:expr) => {{ - eprintln!("Comparing r"); - compare!($a[0], $b[0]); - eprintln!("Comparing g"); - compare!($a[1], $b[1]); - eprintln!("Comparing b"); - compare!($a[2], $b[2]); - }}; - } - - macro_rules! compare_hsl_rgb { - ($h:expr, $s:expr, $rgb:expr) => {{ - let sat = $s; - compare_rgb!(XY::rgb_from_hsl(HS { hue: $h, sat }, 0.5), $rgb); - }}; - } + use crate::{ + WIDE_GAMUT_MAX_X, WIDE_GAMUT_MAX_Y, compare, compare_float, compare_hsl_rgb, compare_rgb, + compare_xy, + }; #[test] fn rgb_from_hsl() { @@ -179,4 +156,100 @@ mod tests { compare_hsl_rgb!(2.5 / 3.0, sat, [ONE, 0.0, ONE]); // blue-red compare_hsl_rgb!(3.0 / 3.0, sat, [ONE, 0.0, 0.0]); // red (wrapped around) } + + #[test] + fn xy_from_f64() { + let a = XY::from([0.1, 0.2]); + let b = XY::new(0.1, 0.2); + + compare!(a.x, b.x); + compare!(a.y, b.y); + } + + #[test] + fn f64_from_xy() { + let a = [0.1, 0.2]; + let b = <[f64; 2]>::from(XY::new(0.1, 0.2)); + + compare!(a[0], b[0]); + compare!(a[1], b[1]); + } + + #[test] + fn xy_from_quant_max() { + let xy = XY::from_quant([0xFF, 0xFF, 0xFF]); + compare!(xy.x, WIDE_GAMUT_MAX_X); + compare!(xy.y, WIDE_GAMUT_MAX_Y); + } + + #[test] + fn xy_from_quant_zero() { + let xy = XY::from_quant([0x00, 0x00, 0x00]); + compare!(xy.x, 0.0); + compare!(xy.y, 0.0); + } + + #[test] + fn xy_from_quant_middle_x() { + let xy = XY::from_quant([0xFF, 0x07, 0x00]); + compare!(xy.x, WIDE_GAMUT_MAX_X / 2.0); + compare!(xy.y, 0.0); + } + + #[test] + fn xy_from_quant_middle_y() { + let xy = XY::from_quant([0x00, 0x00, 0x80]); + compare!(xy.x, 0.0); + compare!(xy.y, WIDE_GAMUT_MAX_Y / 2.0 + 0.0001); + } + + #[test] + fn xy_to_quant_middle_x() { + let xy = XY::new(WIDE_GAMUT_MAX_X / 2.0, 0.0); + + assert_eq!(xy.to_quant(), [0xFF, 0x07, 0x00]); + } + + #[test] + fn xy_to_quant_middle_y() { + let xy = XY::new(0.0, WIDE_GAMUT_MAX_Y / 2.0); + + assert_eq!(xy.to_quant(), [0x00, 0xF0, 0x7F]); + } + + #[test] + fn xy_from_rgb_unit_black() { + let (xy, b) = XY::from_rgb_unit(0.0, 0.0, 0.0); + compare!(b, 0.0); + compare!(xy.x, XY::D50_WHITE_POINT.x); + compare!(xy.y, XY::D50_WHITE_POINT.y); + } + + #[test] + fn xy_from_rgb_unit_white() { + let (xy, b) = XY::from_rgb_unit(1.0, 1.0, 1.0); + compare!(b, 255.0); + compare!(xy.x, XY::D50_WHITE_POINT.x); + compare!(xy.y, XY::D50_WHITE_POINT.y); + } + + #[test] + fn xy_to_rgb_white() { + let xy = XY::D50_WHITE_POINT; + assert_eq!(xy.to_rgb(255.0), [0xFF, 0xFF, 0xFF]); + } + + #[test] + fn xy_from_hs() { + let (xy, b) = XY::from_hs(HS { hue: 0.0, sat: 0.0 }); + compare_float!(b, 255.0 / 2.0, 1e-2); + compare_xy!(xy, XY::D50_WHITE_POINT); + } + + #[test] + fn xy_from_hsl() { + let (xy, b) = XY::from_hsl(HS { hue: 0.0, sat: 0.0 }, 1.0); + compare!(b, 255.0); + compare_xy!(xy, XY::D50_WHITE_POINT); + } } diff --git a/crates/hue/src/zigbee/composite.rs b/crates/hue/src/zigbee/composite.rs index b7891eda..3508c536 100644 --- a/crates/hue/src/zigbee/composite.rs +++ b/crates/hue/src/zigbee/composite.rs @@ -26,6 +26,7 @@ pub enum EffectType { Enchant = 0x11, } +#[cfg_attr(coverage_nightly, coverage(off))] impl From for EffectType { fn from(value: LightEffect) -> Self { match value { @@ -44,13 +45,14 @@ impl From for EffectType { } } -#[derive(PrimitiveEnum_u8, Debug, Copy, Clone)] +#[derive(PrimitiveEnum_u8, Debug, Copy, Clone, PartialEq, Eq)] pub enum GradientStyle { Linear = 0x00, Scattered = 0x02, Mirrored = 0x04, } +#[cfg_attr(coverage_nightly, coverage(off))] impl From for GradientStyle { fn from(value: LightGradientMode) -> Self { match value { @@ -375,3 +377,317 @@ impl HueZigbeeUpdate { Ok(()) } } + +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::error::HueError; + use crate::xy::XY; + use crate::zigbee::{EffectType, GradientParams, GradientStyle, HueZigbeeUpdate}; + use crate::{compare, compare_float, compare_xy, compare_xy_quant}; + + #[test] + fn hzb_none() { + let hz = HueZigbeeUpdate::new(); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x00, 0x00]); + } + + #[test] + fn hzb_onoff() { + let hz = HueZigbeeUpdate::new().with_on_off(true); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x01, 0x00, 0x01]); + } + + #[test] + fn hzb_brightness() { + let hz = HueZigbeeUpdate::new().with_brightness(0x42); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x02, 0x00, 0x42]); + } + + #[test] + fn hzb_mirek() { + let hz = HueZigbeeUpdate::new().with_color_mirek(0x1234); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x04, 0x00, 0x34, 0x12]); + } + + #[test] + fn hzb_xy() { + let hz = HueZigbeeUpdate::new().with_color_xy(XY::new(0.5, 1.0)); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x08, 0x00, 0xFF, 0x7F, 0xFF, 0xFF]); + } + + #[test] + fn hzb_fade_speed() { + let hz = HueZigbeeUpdate::new().with_fade_speed(0x1234); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x10, 0x00, 0x34, 0x12]); + } + + #[test] + fn hzb_effect_type() { + let hz = HueZigbeeUpdate::new().with_effect_type(EffectType::Candle); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x20, 0x00, 0x01]); + } + + #[test] + fn hzb_gradient_empty() { + let hz = HueZigbeeUpdate::new() + .with_gradient_colors(GradientStyle::Scattered, vec![]) + .unwrap(); + let bytes = hz.to_vec().unwrap(); + assert_eq!( + bytes, + &[ + 0x00, 0x01, // flags + 0x04, // data length + 0x00, // number of lights (<< 4) + 0x02, // style: scattered + 0x00, 0x00 // padding + ] + ); + } + + #[test] + fn hzb_gradient_lights() { + let col1 = XY::new(0.5, 0.5); + let hz = HueZigbeeUpdate::new() + .with_gradient_colors(GradientStyle::Scattered, vec![col1]) + .unwrap(); + let bytes = hz.to_vec().unwrap(); + let quant = col1.to_quant(); + assert_eq!( + bytes, + &[ + 0x00, 0x01, // flags + 0x07, // data length + 0x10, // number of lights (<< 4) + 0x02, // style: scattered + 0x00, 0x00, // padding + quant[0], quant[1], quant[2], + ] + ); + } + + #[test] + fn hzb_gradient_too_many() { + let col = XY::new(0.5, 0.5); + let res = HueZigbeeUpdate::new() + .with_gradient_colors(GradientStyle::Scattered, [col].repeat(257)); + assert!(matches!(res, Err(HueError::TryFromIntError(_)))); + } + + #[test] + fn hzb_effect_speed() { + let hz = HueZigbeeUpdate::new().with_effect_speed(0xAB); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x80, 0x00, 0xAB]); + } + + #[test] + fn hzb_gradient_params() { + let hz = HueZigbeeUpdate::new().with_gradient_params(GradientParams { + scale: 0x12, + offset: 0x34, + }); + let bytes = hz.to_vec().unwrap(); + + assert_eq!(bytes, &[0x40, 0x00, 0x12, 0x34]); + } + + #[test] + fn hzb_is_empty() { + use HueZigbeeUpdate as HZU; + assert!(HZU::new().is_empty()); + assert!(!HZU::new().with_on_off(false).is_empty()); + assert!(!HZU::new().with_brightness(0x01).is_empty()); + assert!(!HZU::new().with_color_mirek(0x01).is_empty()); + assert!(!HZU::new().with_color_xy(XY::D50_WHITE_POINT).is_empty()); + assert!(!HZU::new().with_color_xy(XY::D50_WHITE_POINT).is_empty()); + assert!(!HZU::new().with_effect_type(EffectType::Cosmos).is_empty(),); + assert!(!HZU::new().with_fade_speed(0x01).is_empty()); + assert!( + !HZU::new() + .with_gradient_colors(GradientStyle::Mirrored, vec![]) + .unwrap() + .is_empty(), + ); + assert!( + !HZU::new() + .with_gradient_params(GradientParams { + scale: 0x01, + offset: 0x02 + }) + .is_empty(), + ); + } + + #[test] + fn hzb_parse_eof() { + let data = []; + let mut cur = Cursor::new(data.as_slice()); + match HueZigbeeUpdate::from_reader(&mut cur) { + Ok(_) => panic!(), + Err(err) => assert!(matches!(err, HueError::IOError(_))), + } + } + + #[test] + fn hzb_parse_empty() { + let data = [0x00, 0x00]; + let mut cur = Cursor::new(data.as_slice()); + let res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_onoff() { + let data = [0x01, 0x00, 0x01]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!(res.onoff.take(), Some(0x01)); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_brightness() { + let data = [0x02, 0x00, 0x42]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!(res.brightness.take(), Some(0x42)); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_mirek() { + let data = [0x04, 0x00, 0x22, 0x11]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!(res.color_mirek.take(), Some(0x1122)); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_xy() { + let data = [0x08, 0x00, 0xFF, 0x7F, 0xFF, 0xFF]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + let xy = res.color_xy.take().unwrap(); + compare_xy!(xy, XY::new(0.5, 1.0)); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_fade_speed() { + let data = [0x10, 0x00, 0x22, 0x11]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!(res.fade_speed.take(), Some(0x1122)); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_effect_type() { + let data = [0x20, 0x00, 0x01]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!( + res.effect_type.take().unwrap() as u8, + EffectType::Candle as u8 + ); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_effect_speed() { + let data = [0x80, 0x00, 0xAB]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + assert_eq!(res.effect_speed.take().unwrap(), 0xAB); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_gradient_params() { + let data = [0x40, 0x00, 0x12, 0x34]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + + let params = res.gradient_params.take().unwrap(); + assert_eq!(params.scale, 0x12); + assert_eq!(params.offset, 0x34); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_gradient_lights() { + let col1 = XY::new(0.70, 0.70); + + let quant = col1.to_quant(); + + let data = [ + 0x00, + 0x01, // flags + 0x07, // data length + 0x10, // number of lights (<< 4) + 0x02, // style: scattered + 0x00, + 0x00, // padding + quant[0] + 0x01, + quant[1], + quant[2], + ]; + let mut cur = Cursor::new(data.as_slice()); + let mut res = HueZigbeeUpdate::from_reader(&mut cur).unwrap(); + let gc = res.gradient_colors.take().unwrap(); + assert_eq!(gc.points.len(), 1); + assert_eq!(gc.header.nlights, 1); + assert_eq!(gc.header.resv0, 0); + assert_eq!(gc.header.resv2, 0); + assert_eq!(gc.header.style, GradientStyle::Scattered); + eprintln!("{:.4?}", gc.points[0]); + compare_xy_quant!(gc.points[0], col1); + assert!(res.is_empty()); + } + + #[test] + fn hzb_parse_unknown_flags() { + let data = [0x00, 0x20]; + let mut cur = Cursor::new(data.as_slice()); + match HueZigbeeUpdate::from_reader(&mut cur) { + Ok(_) => panic!(), + Err(err) => assert!(matches!(err, HueError::HueZigbeeUnknownFlags(_))), + } + } + + #[test] + fn grad_params_new_is_default() { + let a = GradientParams::new(); + let b = GradientParams::default(); + assert_eq!(a.offset, b.offset); + assert_eq!(a.scale, b.scale); + } +} diff --git a/crates/hue/src/zigbee/entertainment.rs b/crates/hue/src/zigbee/entertainment.rs index 85a6796b..14e2a309 100644 --- a/crates/hue/src/zigbee/entertainment.rs +++ b/crates/hue/src/zigbee/entertainment.rs @@ -83,6 +83,15 @@ impl HueEntFrameLightRecord { self.brightness >> 5 } + #[must_use] + pub const fn mode(&self) -> Option { + match self.brightness & 0x1F { + val if val == LightRecordMode::Device as u16 => Some(LightRecordMode::Device), + val if val == LightRecordMode::Segment as u16 => Some(LightRecordMode::Segment), + _ => None, + } + } + #[must_use] pub const fn raw(&self) -> [u8; 3] { self.raw @@ -177,9 +186,9 @@ impl HueEntSegmentLayout { pub fn pack(&self) -> HueResult> { let mut res = vec![]; - let count = u16::try_from(self.members.len())?; + let count = u8::try_from(self.members.len())?; res.write_u16::(0)?; - res.write_u16::(count)?; + res.write_u8(count)?; for m in &self.members { res.write_all(&m.pack()?)?; } @@ -229,7 +238,12 @@ impl HueEntFrame { mod tests { use packed_struct::prelude::*; - use crate::zigbee::{HueEntFrameLightRecord, LightRecordMode}; + use crate::error::HueError; + use crate::xy::XY; + use crate::zigbee::{ + HueEntFrame, HueEntFrameLightRecord, HueEntSegment, HueEntSegmentConfig, + HueEntSegmentLayout, LightRecordMode, + }; #[test] fn light_record() { @@ -267,4 +281,131 @@ mod tests { assert_eq!("2211ebffaabbcc", hex::encode(data)); } + + #[test] + fn light_brightness() { + let data = hex::decode("2211e0ffaabbcc").unwrap(); + let rec = HueEntFrameLightRecord::unpack_from_slice(&data).unwrap(); + assert_eq!(rec.addr, 0x1122); + assert_eq!(rec.brightness(), 0x7FF); + } + + #[test] + fn light_raw() { + let val = HueEntFrameLightRecord { + addr: 0, + brightness: 0, + raw: [1, 2, 3], + }; + assert_eq!(val.raw(), [1, 2, 3]); + } + + #[test] + fn light_record_mode() { + let val = HueEntFrameLightRecord { + addr: 0, + brightness: LightRecordMode::Device as u16, + raw: [0, 0, 0], + }; + assert_eq!(val.mode(), Some(LightRecordMode::Device)); + + let val = HueEntFrameLightRecord { + addr: 0, + brightness: LightRecordMode::Segment as u16, + raw: [0, 0, 0], + }; + assert_eq!(val.mode(), Some(LightRecordMode::Segment)); + + let val = HueEntFrameLightRecord { + addr: 0, + brightness: 1, + raw: [0, 0, 0], + }; + assert_eq!(val.mode(), None); + } + + #[test] + fn light_debug() { + let xy = XY::new(0.1, 0.2); + let val = + HueEntFrameLightRecord::new(0x1234, 0x7FF, LightRecordMode::Device, xy.to_quant()); + assert_eq!(format!("{val:?}"), "<1234> (0.100,0.200)@ffeb"); + } + + #[test] + fn hue_ent_segment_config() { + let cfg = HueEntSegmentConfig::new(&[1, 2, 3, 4]); + let data = cfg.pack().unwrap(); + assert_eq!(data, [0x00, 0x04, 1, 0, 2, 0, 3, 0, 4, 0]); + + let rev = HueEntSegmentConfig::parse(&data).unwrap(); + assert_eq!(rev.members, [1, 2, 3, 4]); + } + + #[test] + fn hue_ent_segment_layout_invalid() { + let err = HueEntSegmentLayout::parse(&[0x00, 0x00]).unwrap_err(); + assert!(matches!(err, HueError::HueZigbeeDecodeError)); + } + + #[test] + fn hue_ent_segment_layout_odd() { + let err = HueEntSegmentLayout::parse(&[0x00, 0x00, 0x01, 0xAA]).unwrap_err(); + assert!(matches!(err, HueError::HueZigbeeDecodeError)); + } + + #[test] + fn hue_ent_segment_layout() { + let cfg = HueEntSegmentLayout::new(&[HueEntSegment { + length: 10, + index: 20, + }]); + let data = cfg.pack().unwrap(); + assert_eq!(data, [0x00, 0x00, 1, 10, 20]); + + let rev = HueEntSegmentLayout::parse(&data).unwrap(); + assert_eq!(rev.members.len(), 1); + assert_eq!(rev.members[0].length, 10); + assert_eq!(rev.members[0].index, 20); + } + + #[test] + fn hue_ent_frame_invalid() { + let data = [0x44, 0x33, 0x22, 0x11]; + let err = HueEntFrame::parse(&data).unwrap_err(); + assert!(matches!(err, HueError::HueZigbeeDecodeError)); + } + + #[test] + fn hue_ent_frame() { + let cfg = HueEntFrame { + counter: 0x11_22_33_44, + smoothing: 0xAA_BB, + blks: vec![HueEntFrameLightRecord { + addr: 0x7788, + brightness: 0x123, + raw: [0xCC, 0xDD, 0xEE], + }], + }; + let data = cfg.pack().unwrap(); + + assert_eq!( + data, + [ + 0x44, 0x33, 0x22, 0x11, // counter + 0xBB, 0xAA, // smoothing + 0x88, 0x77, // addr + 0x23, 0x01, // brightness + 0xCC, 0xDD, 0xEE // raw + ] + ); + + let rev = HueEntFrame::parse(&data).unwrap(); + assert_eq!(rev.counter, 0x11_22_33_44); + assert_eq!(rev.smoothing, 0xAA_BB); + assert_eq!(rev.blks.len(), 1); + assert_eq!(rev.blks[0].addr, 0x7788); + assert_eq!(rev.blks[0].brightness, 0x123); + assert_eq!(rev.blks[0].raw(), [0xCC, 0xDD, 0xEE]); + } } diff --git a/crates/hue/src/zigbee/stream.rs b/crates/hue/src/zigbee/stream.rs index 2ce74cf2..c1b56995 100644 --- a/crates/hue/src/zigbee/stream.rs +++ b/crates/hue/src/zigbee/stream.rs @@ -161,38 +161,135 @@ mod tests { use chrono::Duration; - use crate::zigbee::EntertainmentZigbeeStream as EZS; + use crate::zigbee::{ + EntertainmentZigbeeStream as EZS, PHILIPS_HUE_ZIGBEE_VENDOR_ID, ZigbeeMessage, + }; + #[allow(clippy::bool_assert_comparison)] #[test] - fn test_duration_zero() { + fn zigbee_message() { + let zb = ZigbeeMessage::new(0x1122, 0x33, vec![0x44, 0x55]); + assert_eq!(zb.cluster, 0x1122); + assert_eq!(zb.command, 0x33); + assert_eq!(zb.data, [0x44, 0x55]); + assert_eq!(zb.ddr, true); + assert_eq!(zb.frametype, 1); + assert_eq!(zb.mfc, Some(PHILIPS_HUE_ZIGBEE_VENDOR_ID)); + + let zb = zb.with_ddr(false); + assert_eq!(zb.ddr, false); + + let zb = zb.with_mfc(None); + assert_eq!(zb.mfc, None); + + let zb = zb.with_mfc(Some(0x1234)); + assert_eq!(zb.mfc, Some(0x1234)); + } + + #[test] + fn entertainment_zigbee_stream_default() { + let val = EZS::default(); + assert_eq!(val.counter, 0); + assert_eq!(val.smoothing, EZS::DEFAULT_SMOOTHING); + } + + #[test] + fn entertainment_zigbee_stream() { + let mut val = EZS { + smoothing: 0x1122, + counter: 0x11_22_33_44, + }; + assert_eq!(val.counter(), 0x11_22_33_44); + assert_eq!(val.smoothing(), 0x1122); + + val.set_smoothing(0x3344); + assert_eq!(val.smoothing(), 0x3344); + + val.set_smoothing_duration(Duration::milliseconds(0)) + .unwrap(); + assert_eq!(val.smoothing(), 0); + } + + #[test] + fn ezs_reset() { + let mut ezs = EZS::new(0x1122); + + let rst = ezs.reset().unwrap(); + assert_eq!(rst.cluster, EZS::CLUSTER); + assert_eq!(rst.command, EZS::CMD_RESET); + assert_eq!(rst.data, [0x00, 0x01, 0x22, 0x11, 0x00, 0x00]); + + // counter should be the same + assert_eq!(ezs.counter(), 0x1122); + } + + #[allow(clippy::bool_assert_comparison, clippy::cast_possible_truncation)] + #[test] + fn ezs_frame() { + let mut ezs = EZS::new(0x1122); + + let rst = ezs.frame(vec![]).unwrap(); + assert_eq!(rst.cluster, EZS::CLUSTER); + assert_eq!(rst.command, EZS::CMD_FRAME); + assert_eq!( + rst.data, + [ + 0x22, + 0x11, + 0x00, + 0x00, + ezs.smoothing as u8, + (ezs.smoothing >> 8) as u8 + ] + ); + + // counter should be incremented + assert_eq!(ezs.counter(), 0x1123); + } + + #[test] + fn ezs_segment_mapping() { + let mut ezs = EZS::new(0x1122); + + let rst = ezs.segment_mapping(&[0xA0A1, 0xB0B1]).unwrap(); + assert_eq!(rst.cluster, EZS::CLUSTER); + assert_eq!(rst.command, EZS::CMD_SEGMENT_MAP); + assert_eq!(rst.data, [0x00, 0x02, 0xA1, 0xA0, 0xB1, 0xB0]); + + // counter should be the same + assert_eq!(ezs.counter(), 0x1122); + } + + #[test] + fn duration_zero() { let zero_dur = Duration::seconds(0); let smo = EZS::duration_to_smoothing(zero_dur).unwrap(); assert_eq!(smo, 0); } #[test] - fn test_duration_half() { + fn duration_half() { let max_dur = Duration::microseconds(EZS::SMOOTHING_MAX_MICROS / 2); let smo = EZS::duration_to_smoothing(max_dur).unwrap(); assert_eq!(smo, 0x8000); } #[test] - fn test_duration_max() { + fn duration_max() { let max_dur = Duration::microseconds(EZS::SMOOTHING_MAX_MICROS - 1); let smo = EZS::duration_to_smoothing(max_dur).unwrap(); assert_eq!(smo, 0xFFFF); } #[test] - fn test_duration_negative() { + fn duration_negative() { let max_dur = Duration::microseconds(-1); let smo = EZS::duration_to_smoothing(max_dur); assert!(smo.is_err()); } #[test] - fn test_duration_over_limit() { + fn duration_over_limit() { let max_dur = Duration::microseconds(EZS::SMOOTHING_MAX_MICROS); let smo = EZS::duration_to_smoothing(max_dur); assert!(smo.is_err()); diff --git a/src/resource.rs b/src/resource.rs index f3b21016..88d6a43b 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -237,6 +237,9 @@ impl Resources { zone.services.remove(link); })?; + // Get id_v1 before deleting + let id_v1 = self.id_v1_scope(&link.rid, self.state.get(&link.rid)?); + // Remove resource from state database self.state.remove(&link.rid)?; @@ -261,7 +264,6 @@ impl Resources { self.state_updates.notify_one(); - let id_v1 = self.state.id_v1(&link.rid); let evt = EventBlock::delete(*link, id_v1)?; self.hue_event_stream.hue_event(evt); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 29351e4f..77dc4822 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -137,7 +137,8 @@ impl IntoResponse for ApiError { HueError::IOError(_) | HueError::HueZigbeeDecodeError | HueError::HueZigbeeEncodeError - | HueError::Undiffable => StatusCode::INTERNAL_SERVER_ERROR, + | HueError::Undiffable + | HueError::Unmergable => StatusCode::INTERNAL_SERVER_ERROR, }, Self::AuxNotFound(_) => StatusCode::NOT_FOUND,