first commit
This commit is contained in:
56
examples/convert-product-data.rs
Normal file
56
examples/convert-product-data.rs
Normal 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
211
examples/ez-parse.rs
Normal 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(())
|
||||
}
|
||||
28
examples/generate-server-cert.rs
Normal file
28
examples/generate-server-cert.rs
Normal 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
27
examples/hz-make.rs
Normal 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
113
examples/hz-parse.rs
Normal 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(())
|
||||
}
|
||||
274
examples/normalize-hue-get.rs
Normal file
274
examples/normalize-hue-get.rs
Normal 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
30
examples/wscat.rs
Normal 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
105
examples/wsinput.rs
Normal 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
159
examples/wsparse.rs
Normal 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
56
examples/z2mdump.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user