Files
rust_bifrost/crates/hue/src/api/light.rs
Beyhan Oğur 427856cd3a first commit
2026-04-26 22:29:38 +03:00

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
}
}