use crate::mqtt::discovery::EntityKind;
pub type ClusterId = u32;
pub type EndpointTypeId = u32;
pub const CLUSTER_OCCUPANCY_SENSING: ClusterId = 0x0406;
pub const CLUSTER_SWITCH: ClusterId = 0x003B;
pub const CLUSTER_BASIC_INFORMATION: ClusterId = 0x0028;
pub const CLUSTER_BOOLEAN_STATE: ClusterId = 0x0045;
pub const CLUSTER_BRIDGED_DEVICE_BASIC_INFORMATION: ClusterId = 0x0039;
pub const DEVICE_TYPE_OCCUPANCY_SENSOR: EndpointTypeId = 0x0107;
pub const DEVICE_TYPE_GENERIC_SWITCH: EndpointTypeId = 0x000F;
pub const DEVICE_TYPE_AGGREGATOR: EndpointTypeId = 0x000E;
pub const DEVICE_TYPE_BRIDGED_NODE: EndpointTypeId = 0x0013;
pub const VENDOR_ATTR_PERSON_COUNT: u32 = 0xFFF1_0001;
pub const EVENT_SWITCH_MULTI_PRESS_COMPLETE: u32 = 0x06;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MatterClusterMapping {
pub cluster: ClusterId,
pub device_type: EndpointTypeId,
pub event_id: Option<u32>,
pub vendor_attr_id: Option<u32>,
pub shares_occupancy_endpoint: bool,
}
pub fn matter_mapping(entity: EntityKind) -> Option<MatterClusterMapping> {
use EntityKind::*;
Some(match entity {
Presence | ZoneOccupancy => MatterClusterMapping {
cluster: CLUSTER_OCCUPANCY_SENSING,
device_type: DEVICE_TYPE_OCCUPANCY_SENSOR,
event_id: None,
vendor_attr_id: None,
shares_occupancy_endpoint: false,
},
PersonCount => MatterClusterMapping {
cluster: CLUSTER_OCCUPANCY_SENSING,
device_type: DEVICE_TYPE_OCCUPANCY_SENSOR,
event_id: None,
vendor_attr_id: Some(VENDOR_ATTR_PERSON_COUNT),
shares_occupancy_endpoint: true,
},
FallDetected | BedExit | MultiRoomTransition => MatterClusterMapping {
cluster: CLUSTER_SWITCH,
device_type: DEVICE_TYPE_GENERIC_SWITCH,
event_id: Some(EVENT_SWITCH_MULTI_PRESS_COMPLETE),
vendor_attr_id: None,
shares_occupancy_endpoint: false,
},
SomeoneSleeping
| RoomActive
| MeetingInProgress
| BathroomOccupied => MatterClusterMapping {
cluster: CLUSTER_OCCUPANCY_SENSING,
device_type: DEVICE_TYPE_OCCUPANCY_SENSOR,
event_id: None,
vendor_attr_id: None,
shares_occupancy_endpoint: false,
},
PossibleDistress | ElderlyInactivityAnomaly | NoMovement => MatterClusterMapping {
cluster: CLUSTER_BOOLEAN_STATE,
device_type: DEVICE_TYPE_OCCUPANCY_SENSOR,
event_id: None,
vendor_attr_id: None,
shares_occupancy_endpoint: false,
},
FallRiskElevated => MatterClusterMapping {
cluster: CLUSTER_BRIDGED_DEVICE_BASIC_INFORMATION,
device_type: DEVICE_TYPE_BRIDGED_NODE,
event_id: None,
vendor_attr_id: Some(0xFFF1_0002),
shares_occupancy_endpoint: false,
},
BreathingRate | HeartRate | MotionLevel | MotionEnergy | PresenceScore | Rssi | PoseKeypoints => return None,
})
}
pub fn entity_on_matter(entity: EntityKind) -> bool {
matter_mapping(entity).is_some()
}
pub fn next_endpoint(base: u16, primitive_index: u16) -> u16 {
base.saturating_add(primitive_index)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presence_maps_to_occupancy_sensor() {
let m = matter_mapping(EntityKind::Presence).unwrap();
assert_eq!(m.cluster, 0x0406); assert_eq!(m.device_type, 0x0107); assert!(m.event_id.is_none());
assert!(m.vendor_attr_id.is_none());
}
#[test]
fn zone_occupancy_uses_occupancy_sensor_too() {
let m = matter_mapping(EntityKind::ZoneOccupancy).unwrap();
assert_eq!(m.cluster, CLUSTER_OCCUPANCY_SENSING);
assert_eq!(m.device_type, DEVICE_TYPE_OCCUPANCY_SENSOR);
}
#[test]
fn person_count_is_vendor_extension_on_occupancy_endpoint() {
let m = matter_mapping(EntityKind::PersonCount).unwrap();
assert_eq!(m.cluster, CLUSTER_OCCUPANCY_SENSING);
assert_eq!(m.vendor_attr_id, Some(0xFFF1_0001));
assert!(m.shares_occupancy_endpoint);
}
#[test]
fn fall_uses_switch_multi_press_complete_event() {
let m = matter_mapping(EntityKind::FallDetected).unwrap();
assert_eq!(m.cluster, CLUSTER_SWITCH);
assert_eq!(m.device_type, DEVICE_TYPE_GENERIC_SWITCH);
assert_eq!(m.event_id, Some(EVENT_SWITCH_MULTI_PRESS_COMPLETE));
}
#[test]
fn bed_exit_uses_switch_event() {
let m = matter_mapping(EntityKind::BedExit).unwrap();
assert_eq!(m.cluster, CLUSTER_SWITCH);
assert!(m.event_id.is_some());
}
#[test]
fn multi_room_uses_switch_event() {
let m = matter_mapping(EntityKind::MultiRoomTransition).unwrap();
assert_eq!(m.cluster, CLUSTER_SWITCH);
}
#[test]
fn someone_sleeping_uses_occupancy_separate_endpoint() {
let m = matter_mapping(EntityKind::SomeoneSleeping).unwrap();
assert_eq!(m.cluster, CLUSTER_OCCUPANCY_SENSING);
assert!(!m.shares_occupancy_endpoint);
}
#[test]
fn distress_uses_boolean_state_not_occupancy() {
let m = matter_mapping(EntityKind::PossibleDistress).unwrap();
assert_eq!(m.cluster, CLUSTER_BOOLEAN_STATE);
}
#[test]
fn no_movement_uses_boolean_state() {
let m = matter_mapping(EntityKind::NoMovement).unwrap();
assert_eq!(m.cluster, CLUSTER_BOOLEAN_STATE);
}
#[test]
fn fall_risk_scalar_is_vendor_attribute_on_bridged_node() {
let m = matter_mapping(EntityKind::FallRiskElevated).unwrap();
assert_eq!(m.cluster, CLUSTER_BRIDGED_DEVICE_BASIC_INFORMATION);
assert!(m.vendor_attr_id.is_some());
}
#[test]
fn biometric_entities_have_no_matter_exposure() {
assert!(matter_mapping(EntityKind::HeartRate).is_none());
assert!(matter_mapping(EntityKind::BreathingRate).is_none());
assert!(matter_mapping(EntityKind::PoseKeypoints).is_none());
}
#[test]
fn rssi_and_motion_continuous_are_mqtt_only() {
assert!(matter_mapping(EntityKind::Rssi).is_none());
assert!(matter_mapping(EntityKind::MotionLevel).is_none());
assert!(matter_mapping(EntityKind::MotionEnergy).is_none());
assert!(matter_mapping(EntityKind::PresenceScore).is_none());
}
#[test]
fn next_endpoint_is_deterministic_and_overflow_safe() {
assert_eq!(next_endpoint(2, 0), 2);
assert_eq!(next_endpoint(2, 5), 7);
assert_eq!(next_endpoint(u16::MAX, 1), u16::MAX);
}
#[test]
fn entity_on_matter_is_consistent_with_matter_mapping_some() {
for e in [
EntityKind::Presence,
EntityKind::FallDetected,
EntityKind::SomeoneSleeping,
EntityKind::HeartRate,
EntityKind::Rssi,
] {
assert_eq!(entity_on_matter(e), matter_mapping(e).is_some());
}
}
#[test]
fn all_entities_exhaustive_classification() {
let known = [
EntityKind::Presence,
EntityKind::PersonCount,
EntityKind::BreathingRate,
EntityKind::HeartRate,
EntityKind::MotionLevel,
EntityKind::MotionEnergy,
EntityKind::FallDetected,
EntityKind::PresenceScore,
EntityKind::Rssi,
EntityKind::ZoneOccupancy,
EntityKind::PoseKeypoints,
EntityKind::SomeoneSleeping,
EntityKind::PossibleDistress,
EntityKind::RoomActive,
EntityKind::ElderlyInactivityAnomaly,
EntityKind::MeetingInProgress,
EntityKind::BathroomOccupied,
EntityKind::FallRiskElevated,
EntityKind::BedExit,
EntityKind::NoMovement,
EntityKind::MultiRoomTransition,
];
for e in known {
let _ = matter_mapping(e); }
}
#[test]
fn cluster_ids_match_matter_spec_1_3() {
assert_eq!(CLUSTER_OCCUPANCY_SENSING, 0x0406);
assert_eq!(CLUSTER_SWITCH, 0x003B);
assert_eq!(CLUSTER_BOOLEAN_STATE, 0x0045);
assert_eq!(CLUSTER_BRIDGED_DEVICE_BASIC_INFORMATION, 0x0039);
assert_eq!(DEVICE_TYPE_OCCUPANCY_SENSOR, 0x0107);
assert_eq!(DEVICE_TYPE_GENERIC_SWITCH, 0x000F);
assert_eq!(DEVICE_TYPE_AGGREGATOR, 0x000E);
assert_eq!(DEVICE_TYPE_BRIDGED_NODE, 0x0013);
}
}