146 lines
4.4 KiB
Rust
146 lines
4.4 KiB
Rust
//! Room-active primitive (§3.12.1 row 3).
|
|
//!
|
|
//! Enter `room_active = ON` when presence is true and motion has been
|
|
//! above `room_active_motion_threshold` (default 10 %) at any point in
|
|
//! a rolling `room_active_window` (default 30 s).
|
|
//!
|
|
//! Exit when no motion above threshold for `room_active_exit_idle`
|
|
//! (default 10 min) OR presence drops false.
|
|
|
|
use std::time::Duration;
|
|
|
|
use super::common::{PrimitiveConfig, PrimitiveState, RawSnapshot, Reason};
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct RoomActive {
|
|
pub active: bool,
|
|
last_motion: Option<Duration>,
|
|
}
|
|
|
|
impl RoomActive {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn tick(&mut self, snap: &RawSnapshot, cfg: &PrimitiveConfig) -> PrimitiveState {
|
|
if snap.since_start < cfg.warmup {
|
|
return PrimitiveState::Idle;
|
|
}
|
|
let above_thresh = snap.motion >= cfg.room_active_motion_threshold;
|
|
if above_thresh && snap.presence {
|
|
self.last_motion = Some(snap.since_start);
|
|
}
|
|
|
|
let recent_motion = matches!(
|
|
self.last_motion,
|
|
Some(t) if snap.since_start.saturating_sub(t) < cfg.room_active_window
|
|
);
|
|
|
|
if !self.active && recent_motion && snap.presence {
|
|
self.active = true;
|
|
return PrimitiveState::Boolean {
|
|
active: true,
|
|
changed: true,
|
|
reason: Reason::new(&["motion>10%", "presence=true", "window<30s"]),
|
|
};
|
|
}
|
|
if self.active {
|
|
let idle_long = matches!(
|
|
self.last_motion,
|
|
Some(t) if snap.since_start.saturating_sub(t) >= cfg.room_active_exit_idle
|
|
) || self.last_motion.is_none();
|
|
if !snap.presence || idle_long {
|
|
self.active = false;
|
|
let mut tags = Vec::new();
|
|
if !snap.presence { tags.push("presence=false"); }
|
|
if idle_long { tags.push("idle>=10min"); }
|
|
return PrimitiveState::Boolean {
|
|
active: false,
|
|
changed: true,
|
|
reason: Reason::new(&tags),
|
|
};
|
|
}
|
|
}
|
|
PrimitiveState::Idle
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn cfg() -> PrimitiveConfig {
|
|
PrimitiveConfig::default()
|
|
}
|
|
|
|
fn snap(t_secs: u64, motion: f64, presence: bool) -> RawSnapshot {
|
|
RawSnapshot {
|
|
since_start: Duration::from_secs(t_secs),
|
|
presence,
|
|
motion,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_fire_during_warmup() {
|
|
let mut p = RoomActive::new();
|
|
let s = snap(30, 0.5, true);
|
|
assert!(matches!(p.tick(&s, &cfg()), PrimitiveState::Idle));
|
|
}
|
|
|
|
#[test]
|
|
fn fires_on_high_motion_with_presence() {
|
|
let mut p = RoomActive::new();
|
|
let s = snap(120, 0.4, true);
|
|
let state = p.tick(&s, &cfg());
|
|
match state {
|
|
PrimitiveState::Boolean { active, changed, .. } => {
|
|
assert!(active);
|
|
assert!(changed);
|
|
}
|
|
other => panic!("expected on/change, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_fire_without_presence() {
|
|
let mut p = RoomActive::new();
|
|
let state = p.tick(&snap(120, 0.4, false), &cfg());
|
|
assert!(matches!(state, PrimitiveState::Idle));
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_fire_below_threshold() {
|
|
let mut p = RoomActive::new();
|
|
let state = p.tick(&snap(120, 0.05, true), &cfg());
|
|
assert!(matches!(state, PrimitiveState::Idle));
|
|
}
|
|
|
|
#[test]
|
|
fn exits_on_presence_drop() {
|
|
let mut p = RoomActive::new();
|
|
let _ = p.tick(&snap(120, 0.4, true), &cfg());
|
|
let state = p.tick(&snap(125, 0.4, false), &cfg());
|
|
match state {
|
|
PrimitiveState::Boolean { active, changed, .. } => {
|
|
assert!(!active);
|
|
assert!(changed);
|
|
}
|
|
other => panic!("expected off/change, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exits_on_extended_idle() {
|
|
let mut p = RoomActive::new();
|
|
let _ = p.tick(&snap(120, 0.4, true), &cfg());
|
|
// Idle below threshold for >10 min.
|
|
let state = p.tick(&snap(120 + 600, 0.02, true), &cfg());
|
|
match state {
|
|
PrimitiveState::Boolean { active, .. } => assert!(!active),
|
|
other => panic!("expected off, got {:?}", other),
|
|
}
|
|
}
|
|
}
|