use std::collections::BTreeSet; use std::ops::{AddAssign, Sub}; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::api::device::DeviceIdentifyUpdate; use crate::api::{DeviceArchetype, Identify, Metadata, MetadataUpdate, ResourceLink, Stub}; use crate::hs::HS; use crate::legacy_api::ApiLightStateUpdate; use crate::xy::XY; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Light { pub owner: ResourceLink, pub metadata: LightMetadata, #[serde(skip_serializing_if = "Option::is_none")] pub product_data: Option, pub alert: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color_temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color_temperature_delta: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dimming: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dimming_delta: Option, pub dynamics: Option, #[serde(skip_serializing_if = "Option::is_none")] pub effects: Option, #[serde(skip_serializing_if = "Option::is_none")] pub effects_v2: Option, #[serde(skip_serializing_if = "Option::is_none")] pub service_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gradient: Option, #[serde(default)] pub identify: Identify, #[serde(skip_serializing_if = "Option::is_none")] pub timed_effects: Option, pub mode: LightMode, pub on: On, #[serde(skip_serializing_if = "Option::is_none")] pub powerup: Option, #[serde(skip_serializing_if = "Option::is_none")] pub signaling: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum LightFunction { Functional, Decorative, Mixed, } #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct LightMetadata { pub name: String, pub archetype: DeviceArchetype, #[serde(skip_serializing_if = "Option::is_none")] pub function: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fixed_mired: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightProductData { #[serde(skip_serializing_if = "Option::is_none")] pub function: Option, } impl LightMetadata { #[must_use] pub fn new(archetype: DeviceArchetype, name: &str) -> Self { Self { archetype, name: name.to_string(), function: Some(LightFunction::Decorative), fixed_mired: None, } } } impl From for Metadata { fn from(value: LightMetadata) -> Self { Self { name: value.name, archetype: value.archetype, } } } impl Light { #[must_use] pub fn new(owner: ResourceLink, metadata: LightMetadata) -> Self { Self { alert: Some(LightAlert { action_values: BTreeSet::from([String::from("breathe")]), }), color: None, color_temperature: None, color_temperature_delta: Some(Stub), dimming: None, dimming_delta: Some(Stub), dynamics: Some(LightDynamics::default()), effects: None, effects_v2: None, service_id: Some(0), gradient: None, identify: Identify {}, timed_effects: Some(LightTimedEffects { status_values: Vec::from(LightTimedEffect::ALL), status: LightTimedEffect::NoEffect, effect_values: Vec::from(LightTimedEffect::ALL), }), mode: LightMode::Normal, on: On { on: true }, product_data: Some(LightProductData { function: Some(LightFunction::Decorative), }), metadata, owner, powerup: Some(LightPowerup { preset: LightPowerupPreset::Safety, configured: true, on: LightPowerupOn::On { on: On { on: true }, }, dimming: LightPowerupDimming::Dimming { dimming: DimmingUpdate { brightness: 100.0 }, }, color: LightPowerupColor::ColorTemperature { color_temperature: ColorTemperatureUpdate::new(366), }, }), signaling: Some(LightSignaling { signal_values: vec![ LightSignal::NoSignal, LightSignal::OnOff, LightSignal::OnOffColor, LightSignal::Alternating, ], status: Value::Null, }), } } #[must_use] pub fn as_dimming_opt(&self) -> Option { self.dimming.as_ref().map(|dim| DimmingUpdate { brightness: dim.brightness, }) } #[must_use] pub fn as_mirek_opt(&self) -> Option { self.color_temperature.as_ref().and_then(|ct| ct.mirek) } #[must_use] pub fn as_color_opt(&self) -> Option { self.color.as_ref().map(|col| col.xy) } #[must_use] pub fn as_gradient_opt(&self) -> Option { self.gradient.as_ref().map(|grad| LightGradientUpdate { mode: Some(grad.mode), points: grad.points.clone(), }) } #[must_use] pub fn is_streaming(&self) -> bool { self.mode == LightMode::Streaming } pub const fn stop_streaming(&mut self) { self.mode = LightMode::Normal; } } impl AddAssign<&LightUpdate> for Light { fn add_assign(&mut self, upd: &LightUpdate) { if let Some(md) = &upd.metadata { if let Some(name) = &md.name { self.metadata.name.clone_from(name); } if let Some(archetype) = &md.archetype { self.metadata.archetype = archetype.clone(); } } if let Some(state) = upd.on { self.on.on = state.on; } if let Some(dim) = &mut self.dimming { if let Some(b) = upd.dimming { dim.brightness = b.brightness; } } if let Some(ct) = &mut self.color_temperature { ct.mirek = upd.color_temperature.and_then(|c| c.mirek); } if let Some(col) = upd.color { if let Some(lcol) = &mut self.color { lcol.xy = col.xy; } if let Some(ct) = &mut self.color_temperature { ct.mirek = None; } } if let Some(grad) = &mut self.gradient { if let Some(grupd) = &upd.gradient { grad.mode = grupd.mode.unwrap_or(grad.mode); grad.points.clone_from(&grupd.points); } } } } #[allow(clippy::if_not_else)] impl Sub<&Light> for &Light { type Output = LightUpdate; fn sub(self, rhs: &Light) -> Self::Output { let mut upd = Self::Output::default(); if self.metadata != rhs.metadata { upd.metadata = Some(MetadataUpdate { name: if self.metadata.name != rhs.metadata.name { Some(rhs.metadata.name.clone()) } else { None }, archetype: if self.metadata.archetype != rhs.metadata.archetype { Some(rhs.metadata.archetype.clone()) } else { None }, function: if self.metadata.function != rhs.metadata.function { rhs.metadata.function.clone() } else { None }, }); } if self.on != rhs.on { upd.on = Some(rhs.on); } if self.dimming != rhs.dimming { upd.dimming = rhs.dimming.map(Into::into); } if self.as_mirek_opt() != rhs.as_mirek_opt() { upd = upd.with_color_temperature(rhs.as_mirek_opt()); } if self.as_color_opt() != rhs.as_color_opt() { upd = upd.with_color_xy(rhs.as_color_opt()); } if self.gradient != rhs.gradient { upd = upd.with_color_xy(rhs.as_color_opt()); } upd } } #[derive(Copy, Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum LightMode { #[default] Normal, Streaming, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightAlert { action_values: BTreeSet, } #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialOrd, Ord, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum LightGradientMode { #[default] InterpolatedPalette, InterpolatedPaletteMirrored, RandomPixelated, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] pub struct LightGradientPoint { pub color: ColorUpdate, } impl LightGradientPoint { #[must_use] pub const fn xy(xy: XY) -> Self { Self { color: ColorUpdate { xy }, } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct LightGradient { pub mode: LightGradientMode, pub mode_values: BTreeSet, pub points_capable: u32, pub points: Vec, pub pixel_count: u32, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LightGradientUpdate { #[serde(default)] pub mode: Option, #[serde(default)] pub points: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LightPowerupPreset { Safety, Powerfail, LastOnState, Custom, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct LightPowerup { pub preset: LightPowerupPreset, pub configured: bool, #[serde(default, skip_serializing_if = "LightPowerupOn::is_none")] pub on: LightPowerupOn, #[serde(default, skip_serializing_if = "LightPowerupDimming::is_none")] pub dimming: LightPowerupDimming, #[serde(default, skip_serializing_if = "LightPowerupColor::is_none")] pub color: LightPowerupColor, } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(tag = "mode", rename_all = "snake_case")] pub enum LightPowerupOn { // Not a real powerup.on.mode option, but used to indicate that // powerup.on itself is null #[default] None, Previous, On { on: On, }, } impl LightPowerupOn { #[must_use] pub const fn is_none(&self) -> bool { matches!(self, Self::None) } } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "mode", rename_all = "snake_case")] pub enum LightPowerupColor { // Not a real powerup.color.mode option, but used to indicate that // powerup.color itself is null #[default] None, Previous, Color { color: ColorUpdate, }, ColorTemperature { color_temperature: ColorTemperatureUpdate, }, } impl LightPowerupColor { #[must_use] pub const fn is_none(&self) -> bool { matches!(self, Self::None) } } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "mode", rename_all = "snake_case")] pub enum LightPowerupDimming { // Not a real powerup.dimming.mode option, but used to indicate that // powerup.dimming itself is null #[default] None, Previous, Dimming { dimming: DimmingUpdate, }, } impl LightPowerupDimming { #[must_use] pub const fn is_none(&self) -> bool { matches!(self, Self::None) } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightSignaling { pub signal_values: Vec, #[serde(default)] #[serde(skip_serializing_if = "Value::is_null")] pub status: Value, } #[derive(Copy, Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LightSignal { #[default] NoSignal, OnOff, OnOffColor, Alternating, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LightDynamicsStatus { DynamicPalette, None, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct LightDynamics { pub status: LightDynamicsStatus, pub status_values: Vec, pub speed: f64, pub speed_valid: bool, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct LightDynamicsUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub speed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, } impl LightDynamicsUpdate { #[must_use] pub fn new() -> Self { Self::default() } #[must_use] pub fn with_duration(self, duration: Option>) -> Self { Self { duration: duration.map(Into::into), ..self } } } impl Default for LightDynamics { fn default() -> Self { Self { status: LightDynamicsStatus::None, status_values: vec![ LightDynamicsStatus::None, LightDynamicsStatus::DynamicPalette, ], speed: 0.0, speed_valid: false, } } } #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LightEffect { #[default] NoEffect, Prism, Opal, Glisten, Sparkle, Fire, Candle, Underwater, Cosmos, Sunbeam, Enchant, } impl LightEffect { pub const ALL: [Self; 11] = [ Self::NoEffect, Self::Candle, Self::Fire, Self::Prism, Self::Sparkle, Self::Opal, Self::Glisten, Self::Underwater, Self::Cosmos, Self::Sunbeam, Self::Enchant, ]; } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightEffects { pub status_values: Vec, pub status: LightEffect, pub effect_values: Vec, } impl LightEffects { #[must_use] pub fn all() -> Self { Self { status_values: Vec::from(LightEffect::ALL), status: LightEffect::NoEffect, effect_values: Vec::from(LightEffect::ALL), } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightEffectsV2 { pub action: LightEffectValues, pub status: LightEffectStatus, } impl LightEffectsV2 { #[must_use] pub fn all() -> Self { Self { action: LightEffectValues { effect_values: Vec::from(LightEffect::ALL), }, status: LightEffectStatus { effect: LightEffect::NoEffect, effect_values: Vec::from(LightEffect::ALL), parameters: None, }, } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LightEffectsUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub action: Option, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LightEffectsV2Update { #[serde(skip_serializing_if = "Option::is_none")] pub action: Option, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct LightEffectActionUpdate { #[serde(default)] pub effect: Option, pub parameters: LightEffectParameters, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct LightEffectParameters { #[serde(default)] pub color: Option, #[serde(default)] pub color_temperature: Option, pub speed: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightEffectValues { pub effect_values: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightEffectStatus { pub effect: LightEffect, pub effect_values: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub parameters: Option, } #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LightTimedEffect { #[default] NoEffect, Sunrise, Sunset, } impl LightTimedEffect { pub const ALL: [Self; 3] = [Self::NoEffect, Self::Sunrise, Self::Sunset]; } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightTimedEffects { pub status_values: Vec, pub status: LightTimedEffect, pub effect_values: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LightTimedEffectsUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub effect: Option, #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct LightUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub on: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dimming: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color_temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gradient: Option, #[serde(skip_serializing_if = "Option::is_none")] pub effects: Option, #[serde(skip_serializing_if = "Option::is_none")] pub effects_v2: Option, #[serde(skip_serializing_if = "Option::is_none")] pub service_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub owner: Option, #[serde(skip_serializing_if = "Option::is_none")] pub powerup: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dynamics: Option, #[serde(skip_serializing_if = "Option::is_none")] pub identify: Option, #[serde(skip_serializing_if = "Option::is_none")] pub timed_effects: Option, } impl LightUpdate { #[must_use] pub fn new() -> Self { Self::default() } #[must_use] pub fn with_brightness(self, dim: Option>) -> Self { Self { dimming: dim.map(Into::into).map(DimmingUpdate::new), ..self } } #[must_use] pub fn with_on(self, on: impl Into>) -> Self { Self { on: on.into(), ..self } } #[must_use] pub fn with_color_temperature(self, mirek: impl Into>) -> Self { Self { color_temperature: mirek.into().map(ColorTemperatureUpdate::new), ..self } } #[must_use] pub fn with_color_xy(self, xy: impl Into>) -> Self { Self { color: self.color.or_else(|| xy.into().map(ColorUpdate::new)), ..self } } #[must_use] pub fn with_color_hs(self, hs: impl Into>) -> Self { Self { color: hs.into().map(|hs| XY::from_hs(hs).0).map(ColorUpdate::new), ..self } } #[must_use] pub fn with_identify(self, identify: Option) -> Self { Self { identify, ..self } } #[must_use] pub fn with_gradient(self, gradient: Option) -> Self { Self { gradient, ..self } } #[must_use] pub fn with_dynamics(self, dynamics: Option) -> Self { Self { dynamics, ..self } } } impl From<&ApiLightStateUpdate> for LightUpdate { fn from(upd: &ApiLightStateUpdate) -> Self { Self::new() .with_on(upd.on.map(On::new)) .with_brightness(upd.bri.map(|b| f64::from(b) / 2.54)) .with_color_temperature(upd.ct) .with_color_hs(upd.hs.map(Into::into)) .with_color_xy(upd.xy.map(Into::into)) .with_dynamics( upd.transitiontime .map(|t| LightDynamicsUpdate::new().with_duration(Some(t * 100))), ) } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] pub struct DimmingUpdate { pub brightness: f64, } impl DimmingUpdate { #[must_use] pub const fn new(brightness: f64) -> Self { Self { brightness } } } impl From for DimmingUpdate { fn from(value: Dimming) -> Self { Self { brightness: value.brightness, } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Delta {} #[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct On { pub on: bool, } impl On { #[must_use] pub const fn new(on: bool) -> Self { Self { on } } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] pub struct ColorUpdate { pub xy: XY, } impl ColorUpdate { #[must_use] pub const fn new(xy: XY) -> Self { Self { xy } } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub struct ColorTemperatureUpdate { pub mirek: Option, } impl ColorTemperatureUpdate { #[must_use] pub const fn new(mirek: u16) -> Self { Self { mirek: Some(mirek) } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ColorGamut { pub red: XY, pub green: XY, pub blue: XY, } impl ColorGamut { pub const GAMUT_C: Self = Self { red: XY { x: 0.6915, y: 0.3083, }, green: XY { x: 0.1700, y: 0.7000, }, blue: XY { x: 0.1532, y: 0.0475, }, }; pub const IKEA_ESTIMATE: Self = Self { red: XY { x: 0.681_235, y: 0.318_186, }, green: XY { x: 0.391_898, y: 0.525_033, }, blue: XY { x: 0.150_241, y: 0.027_116, }, }; } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum GamutType { A, B, C, #[serde(rename = "other")] Other, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct LightColor { #[serde(skip_serializing_if = "Option::is_none")] pub gamut: Option, pub gamut_type: GamutType, pub xy: XY, } impl LightColor { #[must_use] pub const fn new(xy: XY) -> Self { Self { gamut: None, gamut_type: GamutType::Other, xy, } } } #[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct MirekSchema { pub mirek_minimum: u32, pub mirek_maximum: u32, } impl MirekSchema { pub const DEFAULT: Self = Self { mirek_minimum: 153, mirek_maximum: 500, }; } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct ColorTemperature { pub mirek: Option, pub mirek_schema: MirekSchema, pub mirek_valid: bool, } impl From for Option { fn from(value: ColorTemperature) -> Self { value.mirek.map(ColorTemperatureUpdate::new) } } #[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Dimming { pub brightness: f64, #[serde(skip_serializing_if = "Option::is_none")] pub min_dim_level: Option, } impl From for f64 { fn from(value: Dimming) -> Self { value.brightness } }