910 lines
24 KiB
Rust
910 lines
24 KiB
Rust
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<LightProductData>,
|
|
|
|
pub alert: Option<LightAlert>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color: Option<LightColor>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color_temperature: Option<ColorTemperature>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color_temperature_delta: Option<Stub>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dimming: Option<Dimming>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dimming_delta: Option<Stub>,
|
|
pub dynamics: Option<LightDynamics>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub effects: Option<LightEffects>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub effects_v2: Option<LightEffectsV2>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub service_id: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub gradient: Option<LightGradient>,
|
|
#[serde(default)]
|
|
pub identify: Identify,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub timed_effects: Option<LightTimedEffects>,
|
|
pub mode: LightMode,
|
|
pub on: On,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub powerup: Option<LightPowerup>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub signaling: Option<LightSignaling>,
|
|
}
|
|
|
|
#[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<LightFunction>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub fixed_mired: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct LightProductData {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub function: Option<LightFunction>,
|
|
}
|
|
|
|
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<LightMetadata> 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<DimmingUpdate> {
|
|
self.dimming.as_ref().map(|dim| DimmingUpdate {
|
|
brightness: dim.brightness,
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn as_mirek_opt(&self) -> Option<u16> {
|
|
self.color_temperature.as_ref().and_then(|ct| ct.mirek)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn as_color_opt(&self) -> Option<XY> {
|
|
self.color.as_ref().map(|col| col.xy)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn as_gradient_opt(&self) -> Option<LightGradientUpdate> {
|
|
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<String>,
|
|
}
|
|
|
|
#[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<LightGradientMode>,
|
|
pub points_capable: u32,
|
|
pub points: Vec<LightGradientPoint>,
|
|
pub pixel_count: u32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct LightGradientUpdate {
|
|
#[serde(default)]
|
|
pub mode: Option<LightGradientMode>,
|
|
#[serde(default)]
|
|
pub points: Vec<LightGradientPoint>,
|
|
}
|
|
|
|
#[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<LightSignal>,
|
|
#[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<LightDynamicsStatus>,
|
|
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<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub duration: Option<u32>,
|
|
}
|
|
|
|
impl LightDynamicsUpdate {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_duration(self, duration: Option<impl Into<u32>>) -> 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<LightEffect>,
|
|
pub status: LightEffect,
|
|
pub effect_values: Vec<LightEffect>,
|
|
}
|
|
|
|
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<LightEffectActionUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub status: Option<LightEffect>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct LightEffectsV2Update {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub action: Option<LightEffectActionUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub status: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LightEffectActionUpdate {
|
|
#[serde(default)]
|
|
pub effect: Option<LightEffect>,
|
|
pub parameters: LightEffectParameters,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LightEffectParameters {
|
|
#[serde(default)]
|
|
pub color: Option<ColorUpdate>,
|
|
#[serde(default)]
|
|
pub color_temperature: Option<ColorTemperatureUpdate>,
|
|
pub speed: Option<f32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct LightEffectValues {
|
|
pub effect_values: Vec<LightEffect>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct LightEffectStatus {
|
|
pub effect: LightEffect,
|
|
pub effect_values: Vec<LightEffect>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub parameters: Option<Value>,
|
|
}
|
|
|
|
#[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<LightTimedEffect>,
|
|
pub status: LightTimedEffect,
|
|
pub effect_values: Vec<LightTimedEffect>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct LightTimedEffectsUpdate {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub effect: Option<LightTimedEffect>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub duration: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
|
pub struct LightUpdate {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub metadata: Option<MetadataUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub on: Option<On>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dimming: Option<DimmingUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color: Option<ColorUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub color_temperature: Option<ColorTemperatureUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub gradient: Option<LightGradientUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub effects: Option<LightEffectsUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub effects_v2: Option<LightEffectsV2Update>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub service_id: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub owner: Option<ResourceLink>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub powerup: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dynamics: Option<LightDynamicsUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub identify: Option<DeviceIdentifyUpdate>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub timed_effects: Option<LightTimedEffectsUpdate>,
|
|
}
|
|
|
|
impl LightUpdate {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_brightness(self, dim: Option<impl Into<f64>>) -> Self {
|
|
Self {
|
|
dimming: dim.map(Into::into).map(DimmingUpdate::new),
|
|
..self
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_on(self, on: impl Into<Option<On>>) -> Self {
|
|
Self {
|
|
on: on.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_color_temperature(self, mirek: impl Into<Option<u16>>) -> Self {
|
|
Self {
|
|
color_temperature: mirek.into().map(ColorTemperatureUpdate::new),
|
|
..self
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_color_xy(self, xy: impl Into<Option<XY>>) -> Self {
|
|
Self {
|
|
color: self.color.or_else(|| xy.into().map(ColorUpdate::new)),
|
|
..self
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_color_hs(self, hs: impl Into<Option<HS>>) -> 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<DeviceIdentifyUpdate>) -> Self {
|
|
Self { identify, ..self }
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_gradient(self, gradient: Option<LightGradientUpdate>) -> Self {
|
|
Self { gradient, ..self }
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_dynamics(self, dynamics: Option<LightDynamicsUpdate>) -> 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<Dimming> 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<u16>,
|
|
}
|
|
|
|
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<ColorGamut>,
|
|
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<u16>,
|
|
pub mirek_schema: MirekSchema,
|
|
pub mirek_valid: bool,
|
|
}
|
|
|
|
impl From<ColorTemperature> for Option<ColorTemperatureUpdate> {
|
|
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<f64>,
|
|
}
|
|
|
|
impl From<Dimming> for f64 {
|
|
fn from(value: Dimming) -> Self {
|
|
value.brightness
|
|
}
|
|
}
|