use std::time::Duration;
use crate::command::{
ColorTemperatureCommand, Command, DimmerCommand, FadeCommand, FadeDurationCommand,
HsbColorCommand, PowerCommand, SchemeCommand, StartupFadeCommand, WakeupDurationCommand,
};
use crate::error::{DeviceError, Error};
use crate::types::{
ColorTemperature, Dimmer, FadeDuration, HsbColor, PowerIndex, PowerState, RgbColor, Scheme,
WakeupDuration,
};
pub const MAX_ROUTINE_STEPS: usize = 30;
#[derive(Debug, Clone)]
pub struct Routine {
steps: Vec<String>,
}
impl Routine {
#[must_use]
pub fn builder() -> RoutineBuilder {
RoutineBuilder::new()
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub(crate) fn to_backlog_command(&self) -> String {
format!("Backlog0 {}", self.steps.join("; "))
}
}
#[derive(Debug, Clone, Default)]
pub struct RoutineBuilder {
steps: Vec<String>,
}
impl RoutineBuilder {
#[must_use]
pub fn new() -> Self {
Self { steps: Vec::new() }
}
#[must_use]
pub fn power_on(self, index: PowerIndex) -> Self {
self.add_command(&PowerCommand::on(index))
}
#[must_use]
pub fn power_off(self, index: PowerIndex) -> Self {
self.add_command(&PowerCommand::off(index))
}
#[must_use]
pub fn power_toggle(self, index: PowerIndex) -> Self {
self.add_command(&PowerCommand::toggle(index))
}
#[must_use]
pub fn set_power(self, index: PowerIndex, state: PowerState) -> Self {
self.add_command(&PowerCommand::Set { index, state })
}
#[must_use]
pub fn set_dimmer(self, value: Dimmer) -> Self {
self.add_command(&DimmerCommand::Set(value))
}
#[must_use]
pub fn set_color_temperature(self, ct: ColorTemperature) -> Self {
self.add_command(&ColorTemperatureCommand::Set(ct))
}
#[must_use]
pub fn set_hsb_color(self, color: HsbColor) -> Self {
self.add_command(&HsbColorCommand::Set(color))
}
#[must_use]
pub fn set_rgb_color(self, color: RgbColor) -> Self {
self.add_command(&HsbColorCommand::Set(color.to_hsb()))
}
#[must_use]
pub fn set_scheme(self, scheme: Scheme) -> Self {
self.add_command(&SchemeCommand::Set(scheme))
}
#[must_use]
pub fn set_wakeup_duration(self, duration: WakeupDuration) -> Self {
self.add_command(&WakeupDurationCommand::Set(duration))
}
#[must_use]
pub fn enable_fade(self) -> Self {
self.add_command(&FadeCommand::Enable)
}
#[must_use]
pub fn disable_fade(self) -> Self {
self.add_command(&FadeCommand::Disable)
}
#[must_use]
pub fn set_fade_duration(self, duration: FadeDuration) -> Self {
self.add_command(&FadeDurationCommand::Set(duration))
}
#[must_use]
pub fn enable_fade_at_startup(self) -> Self {
self.add_command(&StartupFadeCommand::Enable)
}
#[must_use]
pub fn disable_fade_at_startup(self) -> Self {
self.add_command(&StartupFadeCommand::Disable)
}
#[must_use]
pub fn delay(mut self, duration: Duration) -> Self {
let deciseconds = (duration.as_millis() / 100).clamp(1, 65535);
self.steps.push(format!("Delay {deciseconds}"));
self
}
pub fn build(self) -> Result<Routine, Error> {
if self.steps.is_empty() {
return Err(Error::Device(DeviceError::InvalidConfiguration(
"routine cannot be empty".to_string(),
)));
}
if self.steps.len() > MAX_ROUTINE_STEPS {
return Err(Error::Device(DeviceError::InvalidConfiguration(format!(
"routine exceeds maximum of {MAX_ROUTINE_STEPS} steps (got {})",
self.steps.len()
))));
}
Ok(Routine { steps: self.steps })
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub fn remaining_capacity(&self) -> usize {
MAX_ROUTINE_STEPS.saturating_sub(self.steps.len())
}
fn add_command<C: Command>(mut self, cmd: &C) -> Self {
self.steps.push(cmd.to_http_command());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_routine_fails() {
let result = Routine::builder().build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::Device(DeviceError::InvalidConfiguration(_))
));
}
#[test]
fn single_action_routine() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.build()
.unwrap();
assert_eq!(routine.len(), 1);
assert!(!routine.is_empty());
assert_eq!(routine.to_backlog_command(), "Backlog0 Power1 ON");
}
#[test]
fn multiple_actions_routine() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.set_dimmer(Dimmer::new(75).unwrap())
.build()
.unwrap();
assert_eq!(routine.len(), 2);
assert_eq!(
routine.to_backlog_command(),
"Backlog0 Power1 ON; Dimmer 75"
);
}
#[test]
fn routine_with_delay() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.delay(Duration::from_millis(500))
.power_off(PowerIndex::one())
.build()
.unwrap();
assert_eq!(routine.len(), 3);
assert_eq!(
routine.to_backlog_command(),
"Backlog0 Power1 ON; Delay 5; Power1 OFF"
);
}
#[test]
fn delay_clamped_to_minimum() {
let routine = Routine::builder()
.delay(Duration::from_millis(50))
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("Delay 1"));
}
#[test]
fn delay_clamped_to_maximum() {
let routine = Routine::builder()
.delay(Duration::from_secs(7000))
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("Delay 65535"));
}
#[test]
fn power_control_methods() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.power_off(PowerIndex::new(2).unwrap())
.power_toggle(PowerIndex::new(3).unwrap())
.set_power(PowerIndex::new(4).unwrap(), PowerState::On)
.build()
.unwrap();
assert_eq!(routine.len(), 4);
let cmd = routine.to_backlog_command();
assert!(cmd.contains("Power1 ON"));
assert!(cmd.contains("Power2 OFF"));
assert!(cmd.contains("Power3 TOGGLE"));
assert!(cmd.contains("Power4 ON"));
}
#[test]
fn dimmer_control() {
let routine = Routine::builder()
.set_dimmer(Dimmer::new(50).unwrap())
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("Dimmer 50"));
}
#[test]
fn color_temperature_control() {
let routine = Routine::builder()
.set_color_temperature(ColorTemperature::WARM)
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("CT 370"));
}
#[test]
fn hsb_color_control() {
let routine = Routine::builder()
.set_hsb_color(HsbColor::red())
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("HSBColor 0,100,100"));
}
#[test]
fn rgb_color_control() {
let routine = Routine::builder()
.set_rgb_color(RgbColor::new(255, 0, 0))
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("HSBColor"));
}
#[test]
fn scheme_control() {
let routine = Routine::builder()
.set_scheme(Scheme::CYCLE_UP)
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("Scheme 2"));
}
#[test]
fn wakeup_duration_control() {
let routine = Routine::builder()
.set_wakeup_duration(WakeupDuration::new(Duration::from_secs(300)).unwrap())
.build()
.unwrap();
assert!(routine.to_backlog_command().contains("WakeupDuration 300"));
}
#[test]
fn fade_control() {
let routine = Routine::builder()
.enable_fade()
.set_fade_duration(FadeDuration::new(Duration::from_secs(20)).unwrap())
.build()
.unwrap();
let cmd = routine.to_backlog_command();
assert!(cmd.contains("Fade 1"));
assert!(cmd.contains("Speed 40"));
}
#[test]
fn fade_at_startup_control() {
let routine = Routine::builder().enable_fade_at_startup().build().unwrap();
assert!(routine.to_backlog_command().contains("SetOption91 1"));
}
#[test]
fn routine_at_max_capacity() {
let mut builder = Routine::builder();
for _ in 0..MAX_ROUTINE_STEPS {
builder = builder.power_toggle(PowerIndex::one());
}
assert_eq!(builder.remaining_capacity(), 0);
let result = builder.build();
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), MAX_ROUTINE_STEPS);
}
#[test]
fn routine_exceeds_max_capacity() {
let mut builder = Routine::builder();
for _ in 0..=MAX_ROUTINE_STEPS {
builder = builder.power_toggle(PowerIndex::one());
}
let result = builder.build();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err,
Error::Device(DeviceError::InvalidConfiguration(msg)) if msg.contains("exceeds maximum")
));
}
#[test]
fn builder_remaining_capacity() {
let builder = Routine::builder()
.power_on(PowerIndex::one())
.power_off(PowerIndex::one());
assert_eq!(builder.remaining_capacity(), MAX_ROUTINE_STEPS - 2);
}
#[test]
fn routine_is_cloneable() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.build()
.unwrap();
let cloned = routine.clone();
assert_eq!(routine.len(), cloned.len());
assert_eq!(routine.to_backlog_command(), cloned.to_backlog_command());
}
#[test]
fn builder_is_cloneable() {
let builder = Routine::builder().power_on(PowerIndex::one());
let cloned = builder.clone();
assert_eq!(builder.len(), cloned.len());
}
#[test]
fn complex_wakeup_routine() {
let routine = Routine::builder()
.power_on(PowerIndex::one())
.enable_fade()
.set_fade_duration(FadeDuration::new(Duration::from_secs(20)).unwrap())
.set_dimmer(Dimmer::new(10).unwrap())
.set_color_temperature(ColorTemperature::WARM)
.delay(Duration::from_secs(60))
.set_dimmer(Dimmer::new(50).unwrap())
.delay(Duration::from_secs(60))
.set_dimmer(Dimmer::new(100).unwrap())
.set_color_temperature(ColorTemperature::NEUTRAL)
.build()
.unwrap();
assert_eq!(routine.len(), 10);
}
}