first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:29:38 +03:00
commit 427856cd3a
176 changed files with 27613 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
// Tool to discover DeviceProductData unknown in bifrost::hue::devicedb.
//
// cat samples/*.json | jq '.data? | .[]? | select(.product_data?.hardware_platform_type) | .product_data' | cargo run --example=convert-product-data
//
// Any output from the above command will be devices currently unknown in the device database.
use std::io::stdin;
use serde_json::Deserializer;
use hue::api::DeviceProductData;
use hue::devicedb::{SimpleProductData, product_data};
fn print_std(obj: DeviceProductData) {
let spd = SimpleProductData {
manufacturer_name: &obj.manufacturer_name,
product_name: &obj.product_name,
product_archetype: obj.product_archetype,
hardware_platform_type: obj.hardware_platform_type.as_deref(),
};
println!(
"{:?} => {},",
obj.model_id,
format!("{spd:?}").replace("SimpleProductData ", "SPD ")
);
}
fn main() {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let stream = Deserializer::from_reader(stdin()).into_iter::<DeviceProductData>();
for obj in stream {
let Ok(obj) = obj else {
continue;
};
let pd = product_data(&obj.model_id);
if pd.is_none() {
if obj.manufacturer_name == DeviceProductData::SIGNIFY_MANUFACTURER_NAME {
if let Some(hpt) = obj.hardware_platform_type {
println!(
"{:?} => SPD::signify({:?}, {:?}, {:?}),",
obj.model_id, obj.product_name, obj.product_archetype, hpt,
);
continue;
}
}
print_std(obj);
}
}
}

211
examples/ez-parse.rs Normal file
View File

@@ -0,0 +1,211 @@
#![allow(clippy::cast_possible_truncation, clippy::match_same_arms)]
use std::fmt::Debug;
use std::io::{Cursor, stdin};
use clap::Parser;
use serde::Deserialize;
use bifrost::error::ApiResult;
use zcl::cluster;
use zcl::error::ZclResult;
use zcl::frame::{ZclFrame, ZclFrameDirection, ZclFrameType};
#[macro_use]
extern crate log;
pub fn f64_str<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::str::FromStr;
let s = String::deserialize(deserializer)?;
f64::from_str(&s).map_err(serde::de::Error::custom)
}
pub fn u16_hex<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(u16::from_be(
u16::from_str_radix(&s, 16).map_err(serde::de::Error::custom)?,
))
}
pub fn u16_hex_opt<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
if let Some(s) = opt {
Ok(Some(u16::from_be(
u16::from_str_radix(&s, 16).map_err(serde::de::Error::custom)?,
)))
} else {
Ok(None)
}
}
pub fn vec_hex_opt<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
if let Some(s) = opt {
Ok(hex::decode(s).map_err(serde::de::Error::custom)?)
} else {
Ok(vec![])
}
}
#[derive(Debug, Deserialize)]
pub struct Record {
/* pub src_mac: String, */
/* pub cmd: Option<String>, */
#[serde(deserialize_with = "f64_str")]
pub time: f64,
pub index: u64,
#[serde(deserialize_with = "u16_hex")]
pub src: u16,
#[serde(deserialize_with = "u16_hex")]
pub dst: u16,
#[serde(deserialize_with = "u16_hex")]
pub cluster: u16,
#[serde(deserialize_with = "vec_hex_opt")]
pub data: Vec<u8>,
}
fn parse(rec: &Record, no_index: bool) -> ZclResult<()> {
if rec.data.is_empty() {
return Ok(());
}
let mut cur = Cursor::new(&rec.data);
let frame = ZclFrame::parse(&mut cur)?;
let data = &rec.data[cur.position() as usize..];
let src = hex::encode(rec.src.to_be_bytes());
let dst = hex::encode(rec.dst.to_be_bytes());
let flags = frame.flags;
let cmd = frame.cmd;
let cls = rec.cluster;
let index = if no_index { 0 } else { rec.index };
let describe = |cat: &str, desc: ZclResult<Option<String>>| {
let dir = if flags.direction == ZclFrameDirection::ClientToServer {
" :>"
} else {
"<: "
};
match desc {
Ok(Some(desc)) => {
if desc.is_empty() {
return;
}
info!(
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} {cat}{desc} {}",
hex::encode(data)
);
}
Ok(None) => {
warn!(
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} {cat}Unknown {}",
hex::encode(data)
);
}
Err(err) => {
error!(
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} FAILED {}: {err}",
hex::encode(data)
);
}
}
};
if frame.flags.frame_type == ZclFrameType::ProfileWide {
describe("", cluster::standard::describe(&frame, data));
return Ok(());
}
match rec.cluster {
0x0003 => describe("Effect:", Ok(cluster::effects::describe(&frame, data))),
0x0004 => describe("Group:", Ok(cluster::groups::describe(&frame, data))),
0x0005 => describe("Scene:", Ok(cluster::scenes::describe(&frame, data))),
0x0006 => describe("OnOff:", Ok(cluster::onoff::describe(&frame, data))),
0x0008 => describe("LevelCtrl:", Ok(cluster::levelctrl::describe(&frame, data))),
0x0019 => {
// suppress OTA messages
}
0x0021 => {
// suppress ZGP (Zigbee Green Power) messages
}
0x0300 => describe("ColorCtrl:", Ok(cluster::colorctrl::describe(&frame, data))),
0x0406 => {
// suppress occypancy sensing
}
0x1000 => describe(
"Commissioning:",
cluster::commissioning::describe(&frame, data),
),
0xFC01 => describe("HueEnt:", cluster::hue_fc01::describe(&frame, data)),
0xFC03 => describe("HueCmp:", cluster::hue_fc03::describe(&frame, data)),
_ => describe("UNKNOWN:", Ok(None)),
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(version, long_about = None)]
#[command(about("Parses hue zigbee frames (as hex-encoded lines on stdin)"))]
struct Args {
/// Ignore packet number (easier diffing, since all packets are numbered 0)
#[arg(short, name = "no-index", default_value_t = false)]
no_index: bool,
}
fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let args = Args::parse();
for line in stdin().lines() {
let line = line?;
match serde_json::from_str::<Record>(line.trim()) {
Ok(data) => {
if let Err(err) = parse(&data, args.no_index) {
error!("Failed parse: {err}");
eprintln!(" {line:<40}");
eprintln!(" {data:?}");
}
}
Err(err) => {
error!("Failed to parse json: {err}");
eprintln!(" {line:<40}");
}
}
}
Ok(())
}

View File

@@ -0,0 +1,28 @@
use std::io::{Write, stdout};
use clap::Parser;
use der::{EncodePem, pem::LineEnding};
use mac_address::MacAddress;
use p256::pkcs8::EncodePrivateKey;
use rsa::rand_core::OsRng;
use bifrost::{error::ApiResult, server::certificate};
#[derive(Debug, Parser)]
struct Cli {
mac: MacAddress,
}
fn main() -> ApiResult<()> {
let args = Cli::parse();
let secret_key = p256::SecretKey::random(&mut OsRng);
let cert = certificate::generate(&secret_key, args.mac)?;
let mut out = stdout().lock();
out.write_all(secret_key.to_pkcs8_pem(LineEnding::LF)?.as_bytes())?;
out.write_all(cert.to_pem(LineEnding::LF)?.as_bytes())?;
Ok(())
}

27
examples/hz-make.rs Normal file
View File

@@ -0,0 +1,27 @@
use hue::api::ColorGamut;
use hue::zigbee::{GradientParams, GradientStyle, HueZigbeeUpdate};
use bifrost::error::ApiResult;
fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let hz = HueZigbeeUpdate::new()
.with_on_off(true)
.with_brightness(0x20)
.with_gradient_colors(
GradientStyle::Linear,
vec![ColorGamut::GAMUT_C.red, ColorGamut::GAMUT_C.red],
)?
.with_gradient_params(GradientParams {
scale: 0x38,
offset: 0x00,
});
println!("{}", hex::encode(&hz.to_vec()?));
Ok(())
}

113
examples/hz-parse.rs Normal file
View File

@@ -0,0 +1,113 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
use std::io::{BufRead, Cursor, stdin};
use itertools::Itertools;
use log::warn;
use packed_struct::PrimitiveEnumDynamicStr;
use hue::zigbee::{Flags, GradientColors, HueZigbeeUpdate};
use bifrost::error::ApiResult;
#[allow(clippy::format_push_string)]
#[must_use]
pub fn present_gradcolors(grad: &GradientColors) -> String {
let mut res = format!(
"{}-{}-{}-{:<9}",
grad.header.nlights,
grad.header.resv0,
grad.header.resv2,
grad.header.style.to_display_str(),
);
for p in &grad.points {
let x = (p.x * 1000.0) as u32;
let y = (p.y * 1000.0) as u32;
res += &format!(" {x:03}.{y:03}");
}
res
}
fn show(data: &[u8]) -> ApiResult<()> {
let flags = Flags::from_bits(u16::from(data[0]) | (u16::from(data[1]) << 8)).unwrap();
let mut cur = Cursor::new(data);
let hz = HueZigbeeUpdate::from_reader(&mut cur)?;
let desc = format!(
" {:04x} : {:2} : {:2} : {:4} : {:11} : {:<10} : {:2} : {:<55} : {:4} : {:4} ",
flags.bits(),
hz.onoff.map(|x| format!("{x:02x}")).unwrap_or_default(),
hz.brightness
.map(|x| format!("{x:02x}"))
.unwrap_or_default(),
hz.color_mirek
.map(|x| format!("{x:04x}"))
.unwrap_or_default(),
hz.color_xy
.map(|xy| { format!("{:.3},{:.3}", xy.x, xy.y) })
.unwrap_or_default(),
hz.effect_type
.map(|x| x.to_display_str())
.unwrap_or_default(),
hz.effect_speed
.map(|x| format!("{x:02x}"))
.unwrap_or_default(),
hz.gradient_colors
.as_ref()
.map(present_gradcolors)
.unwrap_or_default(),
hz.gradient_params
.map(|gt| format!("{:02x}{:02x}", gt.scale, gt.offset))
.unwrap_or_default(),
hz.fade_speed
.map(|x| format!("{x:04x}"))
.unwrap_or_default(),
);
println!(
"|{desc}| {} {}",
hex::encode(cur.fill_buf()?),
flags.iter_names().map(|(name, _)| name).join(" | ")
);
Ok(())
}
fn check(orig: &[u8]) -> ApiResult<()> {
let mut cur = Cursor::new(orig);
let hz = HueZigbeeUpdate::from_reader(&mut cur)?;
let mut dest = Cursor::new(vec![]);
hz.serialize(&mut dest)?;
let data = dest.into_inner();
if orig != data {
warn!("DIFF:");
warn!(" {} before", hex::encode(orig));
warn!(" {} after", hex::encode(&data));
}
Ok(())
}
fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
eprintln!(
"| flag | on | br | mrek | (colx,coly) | effect ty. | es | gradient data | grad | fade |"
);
for line in stdin().lines() {
let line = line?;
let data = hex::decode(&line)?;
println!("==================== {line:<40}");
show(&data)?;
check(&data)?;
}
Ok(())
}

View File

@@ -0,0 +1,274 @@
use std::collections::HashMap;
use clap::Parser;
use clap_stdin::FileOrStdin;
use json_diff_ng::compare_serde_values;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{Deserializer, Value};
use bifrost::error::ApiResult;
use hue::api::ResourceRecord;
use hue::legacy_api::{
ApiConfig, ApiGroup, ApiLight, ApiResourceLink, ApiRule, ApiScene, ApiSchedule, ApiSensor,
};
fn false_positive((a, b): &(&Value, &Value)) -> bool {
a.is_number() && b.is_number() && a.as_f64() == b.as_f64()
}
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Input {
V1 {
config: Value,
groups: HashMap<String, Value>,
lights: HashMap<String, Value>,
resourcelinks: HashMap<String, Value>,
rules: HashMap<String, Value>,
scenes: HashMap<String, Value>,
schedules: HashMap<String, Value>,
sensors: HashMap<String, Value>,
},
V2 {
errors: Vec<Value>,
data: Vec<Value>,
},
V2Flat(Value),
}
fn compare(before: &Value, after: &Value, report: bool) -> ApiResult<bool> {
let diffs = compare_serde_values(before, after, true, &[]).unwrap();
let all_diffs = diffs.all_diffs();
if !all_diffs
.iter()
.any(|x| x.1.values.is_none_or(|q| !false_positive(&q)))
{
return Ok(true);
}
/* in report mode, hide diff details */
if report {
return Ok(false);
}
log::error!("Difference detected");
eprintln!("--------------------------------------------------------------------------------");
println!("{}", serde_json::to_string(before)?);
eprintln!("--------------------------------------------------------------------------------");
println!("{}", serde_json::to_string(after)?);
eprintln!("--------------------------------------------------------------------------------");
for (d_type, d_path) in all_diffs {
if let Some(ref ab) = d_path.values {
if false_positive(ab) {
continue;
}
}
match d_type {
json_diff_ng::DiffType::LeftExtra => {
eprintln!(" - {d_path}");
}
json_diff_ng::DiffType::Mismatch => {
eprintln!(" * {d_path}");
}
json_diff_ng::DiffType::RightExtra => {
eprintln!(" + {d_path}");
}
json_diff_ng::DiffType::RootMismatch => {
eprintln!("{d_type}: {d_path}");
}
}
}
eprintln!();
Ok(false)
}
pub struct Normalizer<'a> {
name: &'a str,
items: usize,
errors: usize,
width: usize,
index: usize,
report: bool,
}
impl<'a> Normalizer<'a> {
#[must_use]
pub const fn new(name: &'a str, width: usize, report: bool) -> Self {
Self {
name,
index: 0,
items: 0,
errors: 0,
width,
report,
}
}
pub const fn error(&mut self) {
self.errors += 1;
}
pub fn parse<T>(&mut self, obj: Value) -> ApiResult<T>
where
T: DeserializeOwned + std::fmt::Debug,
{
let data: Result<T, _> = serde_json::from_value(obj);
Ok(data.inspect_err(|err| {
self.errors += 1;
log::error!(
"{name:width$} | >> Parse error {err} (object index {index})",
name = self.name,
width = self.width,
index = self.index
);
/* eprintln!("{}", &serde_json::to_string(&before)?); */
})?)
}
fn roundtrip<T>(&mut self, item: &Value) -> ApiResult<()>
where
T: Serialize + DeserializeOwned + std::fmt::Debug,
{
let value = self.parse::<T>(item.clone())?;
self.items += 1;
self.index += 1;
let after = serde_json::to_value(&value)?;
if !compare(item, &after, self.report)? {
self.errors += 1;
}
Ok(())
}
fn test<T>(&mut self, item: &Value)
where
T: Serialize + DeserializeOwned + std::fmt::Debug,
{
let _ = self.roundtrip::<T>(item);
}
pub fn summary(&self) {
let errors = self.errors;
let items = self.items;
let name = self.name;
let width = self.width;
if errors > 0 {
log::error!("{name:width$} | {items:5} items | {errors:5} errors |");
} else {
log::info!("{name:width$} | {items:5} items | OK |");
}
}
}
fn process_file(file: FileOrStdin, width: usize, report: bool) -> ApiResult<()> {
let name = if file.is_stdin() {
"<stdin>"
} else {
&file.filename().to_string()
};
let stream = Deserializer::from_reader(file.into_reader().unwrap()).into_iter::<Value>();
let mut nml = Normalizer::new(name, width, report);
for obj in stream {
let obj = obj?;
let Ok(msg) = nml.parse::<Input>(obj) else {
continue;
};
match msg {
Input::V1 {
config,
groups,
lights,
resourcelinks,
rules,
scenes,
schedules,
sensors,
} => {
/* log::info!("v1 detected"); */
nml.test::<ApiConfig>(&config);
for item in groups.values() {
nml.test::<ApiGroup>(item);
}
for item in lights.values() {
nml.test::<ApiLight>(item);
}
for item in resourcelinks.values() {
nml.test::<ApiResourceLink>(item);
}
for item in rules.values() {
nml.test::<ApiRule>(item);
}
for item in scenes.values() {
nml.test::<ApiScene>(item);
}
for item in schedules.values() {
nml.test::<ApiSchedule>(item);
}
for item in sensors.values() {
nml.test::<ApiSensor>(item);
}
}
Input::V2 { data, .. } => {
/* log::info!("v2 detected"); */
for item in data {
nml.test::<ResourceRecord>(&item);
}
}
Input::V2Flat(item) => {
/* log::info!("v2flat detected"); */
nml.test::<ResourceRecord>(&item);
}
}
}
nml.summary();
Ok(())
}
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
/// input files
#[arg(name = "files")]
files: Vec<FileOrStdin>,
/// show only per-file summary
#[arg(short, name = "report", default_value_t = false)]
report: bool,
}
impl Args {
pub fn longest_filename(&self) -> usize {
self.files
.iter()
.map(|b| b.filename().len().max(5))
.max()
.unwrap_or(5)
}
}
fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let args = Args::parse();
let width = args.longest_filename();
for file in args.files {
process_file(file, width, args.report)?;
}
Ok(())
}

30
examples/wscat.rs Normal file
View File

@@ -0,0 +1,30 @@
use clap::Parser;
use futures::StreamExt;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use bifrost::error::ApiResult;
#[derive(Parser, Debug)]
struct Args {
/// Url to websocket (<ws://example.org:1234/>)
url: String,
}
#[tokio::main]
async fn main() -> ApiResult<()> {
let args = Args::parse();
let (mut socket, _) = connect_async(args.url).await?;
loop {
let Some(pkt) = socket.next().await else {
break;
};
let Message::Text(txt) = pkt? else { break };
println!("{txt}");
}
Ok(())
}

105
examples/wsinput.rs Normal file
View File

@@ -0,0 +1,105 @@
use std::io::{IsTerminal, Write};
use clap::Parser;
use futures::{SinkExt, StreamExt};
use serde::Deserialize;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, BufReader, stdin};
use tokio::select;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use bifrost::error::ApiResult;
use z2m::api::RawMessage;
#[macro_use]
extern crate log;
#[derive(Parser, Debug)]
struct Args {
/// Url to websocket (<ws://example.org:1234/>)
url: String,
}
#[derive(Deserialize)]
pub struct Log {
pub level: String,
message: String,
}
#[allow(clippy::redundant_pub_crate)]
#[tokio::main]
async fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.write_style(pretty_env_logger::env_logger::fmt::WriteStyle::Always)
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let args = Args::parse();
let (mut socket, _) = connect_async(args.url).await?;
let tty = std::io::stdout().is_terminal();
let mut lines = BufReader::new(stdin()).lines();
loop {
if !tty {
print!("> ");
std::io::stdout().flush()?;
}
select! {
Ok(Some(line)) = lines.next_line() => {
if line.is_empty() {
continue;
}
/* println!("{line}"); */
let req: RawMessage = match serde_json::from_str(&line) {
Ok(res) => res,
Err(err) => {
error!("Failed to parse: {err}");
continue
},
};
/* info!("req: {req:?}"); */
match serde_json::to_string(&req) {
Ok(pkt) => {
/* println!("Parsed: {}", &pkt); */
socket.send(Message::text(pkt)).await?;
}
Err(err) => {
error!("Nope: {err}");
}
}
}
Some(Ok(pkt)) = socket.next() => {
if let Message::Text(txt) = pkt {
let msg: RawMessage = serde_json::from_str(&txt)?;
if msg.topic != "bridge/info" && msg.topic != "bridge/definitions" && msg.topic != "bridge/devices" {
if msg.topic == "bridge/logging" {
let log: Log = serde_json::from_value(msg.payload)?;
if log.message.starts_with("zhc:tz: Read result") {
let body = &log.message.split('\'').nth(2).unwrap()[2..];
info!("{body}");
let rsp: Value = serde_json::from_str(body)?;
info!("{rsp:?}");
} else if log.message.contains("UNSUPPORTED_ATTRIBUTE") {
error!("Unsupported attribute");
} else {
info!("{:?}", log.message);
}
} else {
println!("{msg:?}");
}
}
} else {
println!("{pkt:?}");
}
}
}
}
}

159
examples/wsparse.rs Normal file
View File

@@ -0,0 +1,159 @@
#![allow(clippy::match_same_arms)]
use std::io::stdin;
use log::LevelFilter;
use bifrost::error::ApiResult;
use z2m::api::{Availability, Message, RawMessage};
use z2m::update::DeviceUpdate;
#[allow(clippy::too_many_lines)]
#[tokio::main]
async fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(LevelFilter::Debug)
.init();
for line in stdin().lines() {
let line = line?;
let raw_data = serde_json::from_str::<RawMessage>(&line);
let Ok(raw_msg) = raw_data else {
log::error!("INVALID LINE: {:#?}", raw_data);
continue;
};
/* 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(obj) => {
println!("{:#?}", obj.config_schema);
}
Message::BridgeLogging(obj) => {
println!("{obj:#?}");
}
Message::BridgeExtensions(obj) => {
println!("{obj:#?}");
}
Message::BridgeDevices(devices) => {
for dev in devices {
println!("{dev:#?}");
}
}
Message::BridgeGroups(obj) => {
println!("{obj:#?}");
}
Message::BridgeDefinitions(obj) => {
println!("{obj:#?}");
}
Message::BridgeState(obj) => {
println!("{obj:#?}");
}
Message::BridgeEvent(obj) => {
println!("{obj:#?}");
}
Message::BridgeConverters(obj) => {
println!("{obj:#?}");
}
Message::BridgeGroupMembersAdd(obj) => {
println!("{obj:#?}");
}
Message::BridgeGroupMembersRemove(obj) => {
println!("{obj:#?}");
}
Message::BridgeOptions(obj) => {
println!("{obj:#?}");
}
Message::BridgeTouchlinkScan(obj) => {
println!("{obj:#?}");
}
Message::BridgePermitJoin(obj) => {
println!("{obj:#?}");
}
Message::BridgeNetworkmap(obj) => {
println!("{obj:#?}");
}
Message::BridgeDeviceConfigureReporting(obj) => {
println!("{obj:#?}");
}
Message::BridgeDeviceRemove(obj) => {
println!("{obj:#?}");
}
Message::BridgeDeviceOptions(obj) => {
println!("{obj:#?}");
}
Message::BridgeDeviceOtaUpdateCheck(obj) => {
println!("{obj:#?}");
}
Message::BridgeConfig(obj) => {
println!("{obj:#?}");
}
Message::BridgeResponseGroupAdd(obj) => {
println!("{obj:#?}");
}
Message::BridgeResponseGroupRemove(obj) => {
println!("{obj:#?}");
}
Message::BridgeResponseGroupRename(obj) => {
println!("{obj:#?}");
}
Message::BridgeResponseGroupOptions(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::<Availability>(raw_msg.payload);
let Ok(_msg) = data else {
log::error!("INVALID LINE [availability]: {}", data.unwrap_err());
eprintln!("{line}");
eprintln!();
continue;
};
continue;
}
/* everything that ends in /action are action events */
if raw_msg.topic.ends_with("/action") {
// FIXME: parse action events
continue;
}
/* everything else: device updates */
let data = serde_json::from_value::<DeviceUpdate>(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(())
}

56
examples/z2mdump.rs Normal file
View File

@@ -0,0 +1,56 @@
use clap::Parser;
use futures::StreamExt;
use hyper::Uri;
use serde::Deserialize;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use bifrost::error::ApiResult;
#[derive(Parser, Debug)]
struct Args {
/// Url to websocket (example: <ws://example.org:8080/>)
url: Uri,
}
#[derive(Debug, Deserialize)]
struct Z2mMessage {
topic: String,
}
#[tokio::main]
async fn main() -> ApiResult<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
let args = match Args::try_parse() {
Ok(args) => args,
Err(err) => {
log::error!("Argument error: {err}");
std::process::exit(1);
}
};
let (mut socket, _) = connect_async(args.url).await?;
loop {
let Some(pkt) = socket.next().await else {
break;
};
let Message::Text(txt) = pkt? else { break };
let json: Z2mMessage = serde_json::from_str(&txt)?;
if json.topic.starts_with("bridge/") {
log::info!("Got message [{}]", json.topic);
println!("{txt}");
} else {
log::info!("No more z2m bridge messages");
break;
}
}
Ok(())
}