use crate::mqtt::discovery::EntityKind;
use super::clusters::{
matter_mapping, MatterClusterMapping, DEVICE_TYPE_AGGREGATOR,
DEVICE_TYPE_BRIDGED_NODE,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Endpoint {
pub endpoint_id: u16,
pub device_type: u32,
pub label: String,
pub clusters: Vec<u32>,
pub vendor_attrs: Vec<u32>,
pub source_entity: Option<EntityKind>,
}
#[derive(Debug, Clone)]
pub struct NodeBranch {
pub node_id: String,
pub friendly_name: String,
pub bridged_node_endpoint: u16,
pub child_endpoints: Vec<Endpoint>,
}
#[derive(Debug, Clone)]
pub struct BridgeTree {
pub root: Endpoint,
pub nodes: Vec<NodeBranch>,
}
pub fn build_bridge_tree(nodes: &[(String, String, Vec<EntityKind>)]) -> BridgeTree {
let root = Endpoint {
endpoint_id: 0,
device_type: DEVICE_TYPE_AGGREGATOR,
label: "RuView Bridge".into(),
clusters: vec![super::clusters::CLUSTER_BASIC_INFORMATION],
vendor_attrs: vec![],
source_entity: None,
};
let mut next_endpoint: u16 = 1;
let mut branches = Vec::with_capacity(nodes.len());
for (node_id, friendly_name, entities) in nodes {
let bridged_node_ep = next_endpoint;
next_endpoint += 1;
let mut children = Vec::new();
let mut presence_endpoint_id: Option<u16> = None;
for entity in entities {
let Some(m) = matter_mapping(*entity) else {
continue; };
if m.shares_occupancy_endpoint {
if let Some(parent_ep) = presence_endpoint_id {
if let Some(parent) = children
.iter_mut()
.find(|c: &&mut Endpoint| c.endpoint_id == parent_ep)
{
if let Some(va) = m.vendor_attr_id {
parent.vendor_attrs.push(va);
}
parent.source_entity.get_or_insert(*entity);
}
continue;
}
}
let ep_id = next_endpoint;
next_endpoint += 1;
let mut ep = Endpoint {
endpoint_id: ep_id,
device_type: m.device_type,
label: format!("{:?}", entity),
clusters: vec![m.cluster, super::clusters::CLUSTER_BASIC_INFORMATION],
vendor_attrs: m.vendor_attr_id.into_iter().collect(),
source_entity: Some(*entity),
};
if matches!(*entity, EntityKind::Presence) {
presence_endpoint_id = Some(ep_id);
}
if let Some(_eid) = m.event_id {
}
children.push(ep);
}
branches.push(NodeBranch {
node_id: node_id.clone(),
friendly_name: friendly_name.clone(),
bridged_node_endpoint: bridged_node_ep,
child_endpoints: children,
});
}
BridgeTree {
root,
nodes: branches,
}
}
impl BridgeTree {
pub fn total_endpoints(&self) -> usize {
let per_node: usize = self
.nodes
.iter()
.map(|n| 1 + n.child_endpoints.len()) .sum();
1 + per_node
}
pub fn endpoint(&self, id: u16) -> Option<EndpointRef<'_>> {
if self.root.endpoint_id == id {
return Some(EndpointRef::Root(&self.root));
}
for n in &self.nodes {
if n.bridged_node_endpoint == id {
return Some(EndpointRef::BridgedNode(n));
}
for child in &n.child_endpoints {
if child.endpoint_id == id {
return Some(EndpointRef::Child { branch: n, child });
}
}
}
None
}
}
pub enum EndpointRef<'a> {
Root(&'a Endpoint),
BridgedNode(&'a NodeBranch),
Child { branch: &'a NodeBranch, child: &'a Endpoint },
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mqtt::discovery::EntityKind::*;
fn fixture() -> Vec<(String, String, Vec<EntityKind>)> {
vec![(
"node_aabb".into(),
"Bedroom".into(),
vec![
Presence,
PersonCount, SomeoneSleeping,
FallDetected,
HeartRate, ],
)]
}
#[test]
fn tree_has_aggregator_root() {
let tree = build_bridge_tree(&fixture());
assert_eq!(tree.root.endpoint_id, 0);
assert_eq!(tree.root.device_type, DEVICE_TYPE_AGGREGATOR);
}
#[test]
fn one_branch_per_node() {
let tree = build_bridge_tree(&fixture());
assert_eq!(tree.nodes.len(), 1);
assert_eq!(tree.nodes[0].node_id, "node_aabb");
assert_eq!(tree.nodes[0].friendly_name, "Bedroom");
assert_eq!(tree.nodes[0].bridged_node_endpoint, 1);
}
#[test]
fn person_count_collapses_onto_presence_endpoint() {
let tree = build_bridge_tree(&fixture());
let branch = &tree.nodes[0];
assert_eq!(branch.child_endpoints.len(), 3);
let presence_ep = branch
.child_endpoints
.iter()
.find(|e| e.source_entity == Some(Presence))
.expect("presence endpoint missing");
assert!(presence_ep
.vendor_attrs
.contains(&super::super::clusters::VENDOR_ATTR_PERSON_COUNT));
}
#[test]
fn biometric_entities_skip_matter_tree() {
let tree = build_bridge_tree(&fixture());
let branch = &tree.nodes[0];
for ep in &branch.child_endpoints {
assert!(
ep.source_entity != Some(HeartRate),
"HeartRate must NOT have a Matter endpoint"
);
assert!(
ep.source_entity != Some(BreathingRate),
"BreathingRate must NOT have a Matter endpoint"
);
}
}
#[test]
fn each_child_carries_basic_information_cluster() {
let tree = build_bridge_tree(&fixture());
for branch in &tree.nodes {
for ep in &branch.child_endpoints {
assert!(
ep.clusters
.contains(&super::super::clusters::CLUSTER_BASIC_INFORMATION),
"every endpoint must declare BasicInformation"
);
}
}
}
#[test]
fn endpoint_ids_are_monotonic_and_unique() {
let tree = build_bridge_tree(&fixture());
let mut all_ids = vec![tree.root.endpoint_id];
for branch in &tree.nodes {
all_ids.push(branch.bridged_node_endpoint);
for ep in &branch.child_endpoints {
all_ids.push(ep.endpoint_id);
}
}
let mut sorted = all_ids.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(all_ids.len(), sorted.len(), "endpoint IDs must be unique");
}
#[test]
fn total_endpoints_matches_explicit_count() {
let tree = build_bridge_tree(&fixture());
assert_eq!(tree.total_endpoints(), 5);
}
#[test]
fn endpoint_lookup_resolves_all_ids() {
let tree = build_bridge_tree(&fixture());
for id in 0..tree.total_endpoints() as u16 {
let er = tree.endpoint(id);
assert!(er.is_some(), "endpoint {} not findable", id);
}
assert!(tree.endpoint(999).is_none());
}
#[test]
fn multi_node_tree_keeps_per_node_isolation() {
let nodes = vec![
("aabb".into(), "Bedroom".into(), vec![Presence, FallDetected]),
("ccdd".into(), "Living".into(), vec![Presence, MeetingInProgress]),
];
let tree = build_bridge_tree(&nodes);
assert_eq!(tree.nodes.len(), 2);
for branch in &tree.nodes {
assert_eq!(branch.child_endpoints.len(), 2);
}
assert_eq!(tree.total_endpoints(), 7);
}
#[test]
fn empty_node_list_yields_just_root() {
let tree = build_bridge_tree(&[]);
assert_eq!(tree.nodes.len(), 0);
assert_eq!(tree.total_endpoints(), 1); }
}