1use crate::accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
17use crate::mapping::AccessoryMapping;
18
19#[derive(Debug, Clone, Default)]
25pub struct EdgeVitals {
26 pub presence: bool,
28 pub motion: bool,
30 pub fall_detected: bool,
32 pub breathing_present: bool,
34 pub ambient_temp_c: Option<f64>,
37}
38
39pub struct RuViewToHapMapper;
42
43impl RuViewToHapMapper {
44 pub fn map(vitals: &EdgeVitals) -> Vec<AccessoryMapping> {
49 let mut out = Vec::with_capacity(4);
50
51 out.push(AccessoryMapping {
53 accessory_type: HapAccessoryType::OccupancySensor,
54 characteristics: vec![(
55 HapCharacteristic::OccupancyDetected,
56 HapCharacteristicValue::UInt8(if vitals.presence || vitals.breathing_present { 1 } else { 0 }),
57 )],
58 });
59
60 out.push(AccessoryMapping {
62 accessory_type: HapAccessoryType::MotionSensor,
63 characteristics: vec![(
64 HapCharacteristic::MotionDetected,
65 HapCharacteristicValue::Bool(vitals.motion),
66 )],
67 });
68
69 out.push(AccessoryMapping {
73 accessory_type: HapAccessoryType::LeakSensor,
74 characteristics: vec![(
75 HapCharacteristic::LeakDetected,
76 HapCharacteristicValue::UInt8(if vitals.fall_detected { 1 } else { 0 }),
77 )],
78 });
79
80 if let Some(temp) = vitals.ambient_temp_c {
82 out.push(AccessoryMapping {
83 accessory_type: HapAccessoryType::TemperatureSensor,
84 characteristics: vec![(
85 HapCharacteristic::CurrentTemperature,
86 HapCharacteristicValue::Float(temp),
87 )],
88 });
89 }
90
91 out
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
99
100 #[test]
101 fn presence_true_maps_to_occupancy_detected_1() {
102 let vitals = EdgeVitals { presence: true, ..Default::default() };
103 let mappings = RuViewToHapMapper::map(&vitals);
104 let occ = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::OccupancySensor).unwrap();
105 assert!(occ.characteristics.contains(&(
106 HapCharacteristic::OccupancyDetected,
107 HapCharacteristicValue::UInt8(1)
108 )));
109 }
110
111 #[test]
112 fn fall_detected_maps_to_leak_sensor() {
113 let vitals = EdgeVitals { fall_detected: true, ..Default::default() };
114 let mappings = RuViewToHapMapper::map(&vitals);
115 let leak = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::LeakSensor).unwrap();
116 assert!(leak.characteristics.contains(&(
117 HapCharacteristic::LeakDetected,
118 HapCharacteristicValue::UInt8(1)
119 )));
120 }
121
122 #[test]
123 fn motion_false_maps_correctly() {
124 let vitals = EdgeVitals { motion: false, ..Default::default() };
125 let mappings = RuViewToHapMapper::map(&vitals);
126 let mot = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::MotionSensor).unwrap();
127 assert!(mot.characteristics.contains(&(
128 HapCharacteristic::MotionDetected,
129 HapCharacteristicValue::Bool(false)
130 )));
131 }
132
133 #[test]
134 fn ambient_temp_emits_temperature_mapping() {
135 let vitals = EdgeVitals { ambient_temp_c: Some(22.5), ..Default::default() };
136 let mappings = RuViewToHapMapper::map(&vitals);
137 let temp = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::TemperatureSensor);
138 assert!(temp.is_some());
139 }
140
141 #[test]
142 fn no_ambient_temp_omits_temperature_mapping() {
143 let vitals = EdgeVitals { ambient_temp_c: None, ..Default::default() };
144 let mappings = RuViewToHapMapper::map(&vitals);
145 assert!(mappings.iter().all(|m| m.accessory_type != HapAccessoryType::TemperatureSensor));
146 }
147
148 #[test]
149 fn breathing_present_triggers_occupancy() {
150 let vitals = EdgeVitals { presence: false, breathing_present: true, ..Default::default() };
151 let mappings = RuViewToHapMapper::map(&vitals);
152 let occ = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::OccupancySensor).unwrap();
153 assert!(occ.characteristics.contains(&(
154 HapCharacteristic::OccupancyDetected,
155 HapCharacteristicValue::UInt8(1)
156 )));
157 }
158}