diff --git a/examples/wsparse.rs b/examples/wsparse.rs index 701b944d..4531f710 100644 --- a/examples/wsparse.rs +++ b/examples/wsparse.rs @@ -2,49 +2,103 @@ use std::io::stdin; -use bifrost::{error::ApiResult, z2m::api::Message}; +use bifrost::{ + error::ApiResult, + z2m::{ + api::{Availability, Message, RawMessage}, + update::DeviceUpdate, + }, +}; +use log::LevelFilter; #[tokio::main] #[rustfmt::skip] async fn main() -> ApiResult<()> { - pretty_env_logger::init(); + pretty_env_logger::formatted_builder().filter_level(LevelFilter::Debug).init(); for line in stdin().lines() { - let data = serde_json::from_str(&line?); + let line = line?; - let Ok(msg) = data else { - log::error!("INVALID LINE: {:#?}", data); + let raw_data = serde_json::from_str::(&line); + + let Ok(raw_msg) = raw_data else { + log::error!("INVALID LINE: {:#?}", raw_data); continue; }; - match msg { - Message::BridgeInfo(ref obj) => { - println!("{:#?}", obj.config_schema); - }, - Message::BridgeLogging(ref obj) => { - println!("{obj:#?}"); - }, - Message::BridgeExtensions(ref obj) => { - println!("{obj:#?}"); - }, - Message::BridgeDevices(ref devices) => { - for dev in devices { - println!("{dev:#?}"); - } - }, - Message::BridgeGroups(ref obj) => { - println!("{obj:#?}"); - }, - Message::BridgeDefinitions(ref obj) => { - println!("{obj:#?}"); - }, - Message::BridgeState(ref obj) => { - println!("{obj:#?}"); - }, - Message::BridgeEvent(ref obj) => { - println!("{obj:#?}"); - }, + /* bridge messages are those on bridge/+ topics */ + + if raw_msg.topic.starts_with("bridge/") { + let data = serde_json::from_str(&line); + + let Ok(msg) = data else { + log::error!("INVALID LINE [bridge]: {:#?}", data); + continue; + }; + + match msg { + Message::BridgeInfo(ref obj) => { + println!("{:#?}", obj.config_schema); + }, + Message::BridgeLogging(ref obj) => { + println!("{obj:#?}"); + }, + Message::BridgeExtensions(ref obj) => { + println!("{obj:#?}"); + }, + Message::BridgeDevices(ref devices) => { + for dev in devices { + println!("{dev:#?}"); + } + }, + Message::BridgeGroups(ref obj) => { + println!("{obj:#?}"); + }, + Message::BridgeDefinitions(ref obj) => { + println!("{obj:#?}"); + }, + Message::BridgeState(ref obj) => { + println!("{obj:#?}"); + }, + Message::BridgeEvent(ref obj) => { + println!("{obj:#?}"); + }, + } + + continue; + } + + /* everything that ends in /availability are online/offline updates */ + + if raw_msg.topic.ends_with("/availability") { + let data = serde_json::from_value::(raw_msg.payload); + + let Ok(msg) = data else { + log::error!("INVALID LINE [availability]: {}", data.unwrap_err()); + eprintln!("{line}"); + eprintln!(); + continue; + }; + + continue; } + + /* everything else: device updates */ + + let data = serde_json::from_value::(raw_msg.payload); + + let Ok(msg) = data else { + log::error!("INVALID LINE [device]: {}", data.unwrap_err()); + eprintln!("{line}"); + eprintln!(); + continue; + }; + + /* having unknown fields is not an error. they are simply not mapped */ + /* if !msg.__.is_empty() { */ + /* log::warn!("Unknown fields found: {:?}", msg.__.keys()); */ + /* } */ + println!("{msg:#?}"); } Ok(()) diff --git a/src/z2m/api.rs b/src/z2m/api.rs index ae4b26ff..6bf133a2 100644 --- a/src/z2m/api.rs +++ b/src/z2m/api.rs @@ -42,6 +42,13 @@ pub enum Message { BridgeExtensions(Value), } +#[derive(Serialize, Deserialize, Clone, Hash, Debug, Copy)] +#[serde(rename_all = "snake_case")] +pub enum Availability { + Online, + Offline, +} + #[derive(Serialize, Deserialize, Clone, Hash)] #[serde(transparent)] pub struct IeeeAddress(#[serde(deserialize_with = "ieee_address")] u64); @@ -89,6 +96,7 @@ pub struct BridgeEvent { pub struct BridgeLogging { pub level: String, pub message: String, + pub topic: Option, } type BridgeGroups = Vec; @@ -209,7 +217,7 @@ pub struct ConfigAdvanced { pub channel: i64, pub elapsed: bool, pub ext_pan_id: Vec, - pub homeassistant_legacy_entity_attributes: bool, + pub homeassistant_legacy_entity_attributes: Option, pub last_seen: String, pub legacy_api: bool, pub legacy_availability_payload: bool, @@ -294,8 +302,8 @@ pub enum PowerSource { pub type BridgeDevices = Vec; +#[allow(clippy::pub_underscore_fields)] #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] pub struct Device { pub description: Option, pub date_code: Option, @@ -315,6 +323,11 @@ pub struct Device { pub supported: Option, #[serde(rename = "type")] pub device_type: String, + + /* all other fields */ + #[serde(skip_serializing_if = "HashMap::is_empty")] + #[serde(default, flatten)] + pub __: HashMap, } impl Device { diff --git a/src/z2m/mod.rs b/src/z2m/mod.rs index 1df3ccc7..65b662b7 100644 --- a/src/z2m/mod.rs +++ b/src/z2m/mod.rs @@ -33,7 +33,7 @@ use crate::model::state::AuxData; use crate::resource::Resources; use crate::z2m::api::{ExposeLight, Message, RawMessage}; use crate::z2m::request::{ClientRequest, Z2mRequest}; -use crate::z2m::update::DeviceUpdate; +use crate::z2m::update::{DeviceColor, DeviceUpdate}; #[derive(Debug)] struct LearnScene { @@ -321,7 +321,7 @@ impl Client { .with_on(devupd.state.map(Into::into)) .with_brightness(devupd.brightness.map(|b| b / 254.0 * 100.0)) .with_color_temperature(devupd.color_temp) - .with_color_xy(devupd.color.map(|col| col.xy)); + .with_color_xy(devupd.color.and_then(|col| col.xy)); *light += upd; })?; @@ -333,8 +333,8 @@ impl Client { let light = res.get::(&rlink)?; let mut color_temperature = None; let mut color = None; - if let Some(col) = upd.color { - color = Some(ColorUpdate { xy: col.xy }); + if let Some(DeviceColor { xy: Some(xy), .. }) = upd.color { + color = Some(ColorUpdate { xy }); } else if let Some(mirek) = upd.color_temp { color_temperature = Some(ColorTemperatureUpdate { mirek }); } diff --git a/src/z2m/update.rs b/src/z2m/update.rs index 1b1d4117..8accd2b3 100644 --- a/src/z2m/update.rs +++ b/src/z2m/update.rs @@ -6,8 +6,8 @@ use serde_json::Value; use crate::hue::api::On; use crate::model::types::XY; +#[allow(clippy::pub_underscore_fields)] #[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[serde(deny_unknown_fields)] pub struct DeviceUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub state: Option, @@ -42,6 +42,11 @@ pub struct DeviceUpdate { pub battery: Option, #[serde(skip_serializing_if = "Option::is_none")] pub transition: Option, + + /* all other fields */ + #[serde(skip_serializing_if = "HashMap::is_empty")] + #[serde(default, flatten)] + pub __: HashMap, } impl DeviceUpdate { @@ -53,7 +58,13 @@ impl DeviceUpdate { #[must_use] pub fn with_state(self, state: Option) -> Self { Self { - state: state.map(DeviceState::from), + state: state.map(|on| { + if on { + DeviceState::On + } else { + DeviceState::Off + } + }), ..self } } @@ -97,7 +108,7 @@ pub struct DeviceColor { pub saturation: Option, #[serde(flatten)] - pub xy: XY, + pub xy: Option, } impl DeviceColor { @@ -108,7 +119,7 @@ impl DeviceColor { s: None, hue: None, saturation: None, - xy, + xy: Some(xy), } } @@ -119,7 +130,7 @@ impl DeviceColor { s: None, hue: Some(h), saturation: Some(s), - xy: XY::new(0.0, 0.0), + xy: None, } } } @@ -181,34 +192,19 @@ pub enum DeviceColorMode { Xy, } -#[derive(Copy, Debug, Serialize, Deserialize, Clone)] +#[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] pub enum DeviceState { On, Off, -} - -impl From for DeviceState { - fn from(value: bool) -> Self { - if value { - Self::On - } else { - Self::Off - } - } -} - -impl From for bool { - fn from(value: DeviceState) -> Self { - match value { - DeviceState::On => true, - DeviceState::Off => false, - } - } + Lock, + Unlock, } impl From for On { fn from(value: DeviceState) -> Self { - Self { on: value.into() } + Self { + on: value == DeviceState::On, + } } }