use mdns_sd::{ServiceDaemon, ServiceInfo};
use tracing::info;
use crate::homeauto::matter::error::{MatterError, MatterResult};
use crate::homeauto::matter::types::MatterDeviceConfig;
pub struct CommissionableAdvertiser {
daemon: ServiceDaemon,
service_fullname: String,
}
impl CommissionableAdvertiser {
pub fn start(config: &MatterDeviceConfig) -> MatterResult<Self> {
let daemon =
ServiceDaemon::new().map_err(|e| MatterError::Mdns(format!("daemon init: {e}")))?;
let txt: &[(&str, &str)] = &[
("D", &config.discriminator.to_string()),
("CM", "1"), ("DN", &config.device_name),
("VP", &format!("{}+{}", config.vendor_id, config.product_id)),
("SII", "5000"), ("SAI", "300"), ("T", "0"), ("PH", "33"), ];
let d_val = config.discriminator.to_string();
let vp_val = format!("{}+{}", config.vendor_id, config.product_id);
let txt_owned: Vec<(&str, String)> = vec![
("D", d_val.clone()),
("CM", "1".to_string()),
("DN", config.device_name.clone()),
("VP", vp_val),
("SII", "5000".to_string()),
("SAI", "300".to_string()),
("T", "0".to_string()),
("PH", "33".to_string()),
];
let _ = txt;
let hostname = gethostname::gethostname().to_string_lossy().to_string();
let host_fqdn = if hostname.is_empty() {
format!("matter-{}.local.", config.discriminator)
} else {
format!("{hostname}.local.")
};
let instance_name = format!("BW-{:04X}", config.discriminator);
const SERVICE_TYPE: &str = "_matterc._udp";
let txt_refs: Vec<(&str, &str)> = txt_owned.iter().map(|(k, v)| (*k, v.as_str())).collect();
let svc = ServiceInfo::new(
SERVICE_TYPE,
&instance_name,
&host_fqdn,
(),
config.port,
txt_refs.as_slice(),
)
.map_err(|e| MatterError::Mdns(format!("ServiceInfo: {e}")))?;
let service_fullname = svc.get_fullname().to_string();
daemon
.register(svc)
.map_err(|e| MatterError::Mdns(format!("register: {e}")))?;
info!(
"Matter mDNS: commissionable '{}' on port {} (discriminator {})",
instance_name, config.port, config.discriminator
);
Ok(Self {
daemon,
service_fullname,
})
}
pub fn stop(&self) -> MatterResult<()> {
self.daemon
.unregister(&self.service_fullname)
.map_err(|e| MatterError::Mdns(format!("unregister: {e}")))?;
Ok(())
}
pub fn service_fullname(&self) -> &str {
&self.service_fullname
}
}
impl Drop for CommissionableAdvertiser {
fn drop(&mut self) {
let _ = self.stop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn build_txt_records(config: &MatterDeviceConfig) -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("D".to_string(), config.discriminator.to_string());
map.insert("CM".to_string(), "1".to_string());
map.insert("DN".to_string(), config.device_name.clone());
map.insert(
"VP".to_string(),
format!("{}+{}", config.vendor_id, config.product_id),
);
map.insert("SII".to_string(), "5000".to_string());
map.insert("SAI".to_string(), "300".to_string());
map.insert("T".to_string(), "0".to_string());
map.insert("PH".to_string(), "33".to_string());
map
}
fn test_config() -> MatterDeviceConfig {
MatterDeviceConfig::builder()
.device_name("Test Light")
.vendor_id(0xFFF1)
.product_id(0x8001)
.discriminator(0xF00)
.passcode(20202021)
.port(5540)
.build()
}
#[test]
fn commissionable_txt_records_include_discriminator() {
let config = test_config();
let txt = build_txt_records(&config);
assert!(
txt.contains_key("D"),
"TXT records must contain key 'D' (discriminator)"
);
assert_eq!(
txt["D"],
config.discriminator.to_string(),
"D must be the discriminator as a decimal string"
);
assert_eq!(
txt["CM"], "1",
"CM must be '1' for standard commissioning mode"
);
assert_eq!(
txt["VP"],
format!("{}+{}", config.vendor_id, config.product_id)
);
assert_eq!(txt["SII"], "5000");
assert_eq!(txt["SAI"], "300");
assert_eq!(txt["T"], "0");
assert_eq!(txt["PH"], "33");
}
#[test]
fn commissionable_service_type_is_matterc_udp() {
let config = test_config();
let instance = format!("BW-{:04X}", config.discriminator);
let expected_suffix = "._matterc._udp.local.";
let fullname = format!("{instance}{expected_suffix}");
assert!(
fullname.ends_with("._matterc._udp.local."),
"commissionable service type must be _matterc._udp, got: {fullname}"
);
assert!(
fullname.starts_with("BW-"),
"instance name must start with BW-"
);
}
}