//! Tools for publishing a [Home Assistant light](https://www.home-assistant.io/integrations/light.mqtt/). use core::{ops::Deref, str}; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; use crate::{ fmt::Debug2Format, homeassistant::{binary_sensor::BinarySensorState, ser::List, Component}, Error, Payload, Publishable, Topic, }; #[derive(Serialize)] #[serde(rename_all = "lowercase")] #[allow(missing_docs)] pub enum SupportedColorMode { OnOff, Brightness, #[serde(rename = "color_temp")] ColorTemp, Hs, Xy, Rgb, Rgbw, Rgbww, White, } #[derive(Serialize, Deserialize, Default)] struct SerializedColor { #[serde(default, skip_serializing_if = "Option::is_none")] h: Option, #[serde(default, skip_serializing_if = "Option::is_none")] s: Option, #[serde(default, skip_serializing_if = "Option::is_none")] x: Option, #[serde(default, skip_serializing_if = "Option::is_none")] y: Option, #[serde(default, skip_serializing_if = "Option::is_none")] r: Option, #[serde(default, skip_serializing_if = "Option::is_none")] g: Option, #[serde(default, skip_serializing_if = "Option::is_none")] b: Option, #[serde(default, skip_serializing_if = "Option::is_none")] w: Option, #[serde(default, skip_serializing_if = "Option::is_none")] c: Option, } #[derive(Deserialize)] struct LedPayload<'a> { state: BinarySensorState, #[serde(default)] brightness: Option, #[serde(default)] color_temp: Option, #[serde(default)] color: Option, #[serde(default)] effect: Option<&'a str>, } /// The color of the light in various forms. #[derive(Serialize)] #[serde(rename_all = "lowercase", tag = "color_mode", content = "color")] #[allow(missing_docs)] pub enum Color { None, Brightness(u8), ColorTemp(u32), Hs { #[serde(rename = "h")] hue: f32, #[serde(rename = "s")] saturation: f32, }, Xy { x: f32, y: f32, }, Rgb { #[serde(rename = "r")] red: u8, #[serde(rename = "g")] green: u8, #[serde(rename = "b")] blue: u8, }, Rgbw { #[serde(rename = "r")] red: u8, #[serde(rename = "g")] green: u8, #[serde(rename = "b")] blue: u8, #[serde(rename = "w")] white: u8, }, Rgbww { #[serde(rename = "r")] red: u8, #[serde(rename = "g")] green: u8, #[serde(rename = "b")] blue: u8, #[serde(rename = "c")] cool_white: u8, #[serde(rename = "w")] warm_white: u8, }, } /// The state of the light. This can be sent to the broker and received as a /// command from Home Assistant. pub struct LightState<'a> { /// Whether the light is on or off. pub state: BinarySensorState, /// The color of the light. pub color: Color, /// Any effect that is applied. pub effect: Option<&'a str>, } impl<'a> LightState<'a> { /// Parses the state from a command payload. pub fn from_payload(payload: &'a Payload) -> Result { let parsed: LedPayload<'a> = match payload.deserialize_json() { Ok(p) => p, Err(e) => { warn!("Failed to deserialize packet: {:?}", Debug2Format(&e)); if let Ok(s) = str::from_utf8(payload) { trace!("{}", s); } return Err(Error::PacketError); } }; let color = if let Some(color) = parsed.color { if let Some(x) = color.x { Color::Xy { x, y: color.y.unwrap_or_default(), } } else if let Some(h) = color.h { Color::Hs { hue: h, saturation: color.s.unwrap_or_default(), } } else if let Some(c) = color.c { Color::Rgbww { red: color.r.unwrap_or_default(), green: color.g.unwrap_or_default(), blue: color.b.unwrap_or_default(), cool_white: c, warm_white: color.w.unwrap_or_default(), } } else if let Some(w) = color.w { Color::Rgbw { red: color.r.unwrap_or_default(), green: color.g.unwrap_or_default(), blue: color.b.unwrap_or_default(), white: w, } } else { Color::Rgb { red: color.r.unwrap_or_default(), green: color.g.unwrap_or_default(), blue: color.b.unwrap_or_default(), } } } else if let Some(color_temp) = parsed.color_temp { Color::ColorTemp(color_temp) } else if let Some(brightness) = parsed.brightness { Color::Brightness(brightness) } else { Color::None }; Ok(LightState { state: parsed.state, color, effect: parsed.effect, }) } } impl Serialize for LightState<'_> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut len = 1; if self.effect.is_some() { len += 1; } match self.color { Color::None => {} Color::Brightness(_) | Color::ColorTemp(_) => len += 1, _ => len += 2, } let mut serializer = serializer.serialize_struct("LightState", len)?; serializer.serialize_field("state", &self.state)?; if let Some(effect) = self.effect { serializer.serialize_field("effect", effect)?; } else { serializer.skip_field("effect")?; } match self.color { Color::None => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.skip_field("color")?; } Color::Brightness(b) => { serializer.skip_field("color_temp")?; serializer.skip_field("color")?; serializer.serialize_field("brightness", &b)? } Color::ColorTemp(c) => { serializer.skip_field("brightness")?; serializer.skip_field("color")?; serializer.serialize_field("color_temp", &c)? } Color::Hs { hue, saturation } => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.serialize_field("color_mode", "hs")?; let color = SerializedColor { h: Some(hue), s: Some(saturation), ..Default::default() }; serializer.serialize_field("color", &color)? } Color::Xy { x, y } => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.serialize_field("color_mode", "xy")?; let color = SerializedColor { x: Some(x), y: Some(y), ..Default::default() }; serializer.serialize_field("color", &color)? } Color::Rgb { red, green, blue } => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.serialize_field("color_mode", "rgb")?; let color = SerializedColor { r: Some(red), g: Some(green), b: Some(blue), ..Default::default() }; serializer.serialize_field("color", &color)? } Color::Rgbw { red, green, blue, white, } => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.serialize_field("color_mode", "rgbw")?; let color = SerializedColor { r: Some(red), g: Some(green), b: Some(blue), w: Some(white), ..Default::default() }; serializer.serialize_field("color", &color)? } Color::Rgbww { red, green, blue, cool_white, warm_white, } => { serializer.skip_field("brightness")?; serializer.skip_field("color_temp")?; serializer.serialize_field("color_mode", "rgbww")?; let color = SerializedColor { r: Some(red), g: Some(green), b: Some(blue), c: Some(cool_white), w: Some(warm_white), ..Default::default() }; serializer.serialize_field("color", &color)? } } serializer.end() } } /// A light entity pub struct Light<'a, const C: usize, const E: usize> { /// The color modes supported by the light. pub supported_color_modes: [SupportedColorMode; C], /// Any effects that can be used. pub effects: [&'a str; E], } impl Serialize for Light<'_, C, E> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut len = 2; if C > 0 { len += 1; } if E > 0 { len += 2; } let mut serializer = serializer.serialize_struct("Light", len)?; serializer.serialize_field("schema", "json")?; if C > 0 { serializer.serialize_field("sup_clrm", &List::new(&self.supported_color_modes))?; } else { serializer.skip_field("sup_clrm")?; } if E > 0 { serializer.serialize_field("effect", &true)?; serializer.serialize_field("fx_list", &List::new(&self.effects))?; } else { serializer.skip_field("effect")?; serializer.skip_field("fx_list")?; } serializer.end() } } impl Component for Light<'_, C, E> { type State = LightState<'static>; fn platform() -> &'static str { "light" } async fn publish_state>( &self, topic: &Topic, state: Self::State, ) -> Result<(), Error> { topic.with_json(state).publish().await } }