use std::{collections::HashMap, time::Duration}; use chrono::{DateTime, Datelike, Local, NaiveTime, Weekday}; use common::{BulbPrefs, Param}; use lighter_lib::{BulbColor, BulbId}; use lighter_manager::manager::{BulbCommand, BulbManager, BulbSelector}; use tokio::{spawn, time::sleep}; use crate::util::DeadMansHandle; use super::LightScript; pub struct Waker { manager: BulbManager, wake_times: HashMap>, wake_tasks: HashMap<(BulbId, Weekday), DeadMansHandle>, } impl Waker { pub fn create(manager: BulbManager) -> Self { Waker { manager, wake_times: Default::default(), wake_tasks: Default::default(), } } } const TIME_FMT: &str = "%H:%M"; impl LightScript for Waker { fn get_params(&mut self, bulb: &BulbId) -> BulbPrefs { let settings = self.wake_times.entry(bulb.clone()).or_default(); let kvs = DAYS_OF_WEEK .iter() .map(|day| { let time = match settings.get(day) { Some(time) => time.format(TIME_FMT).to_string(), None => String::new(), }; (format!("{day:?}"), Param::String(time)) }) .collect(); BulbPrefs { kvs } } fn set_param(&mut self, bulb: &BulbId, name: &str, time: super::Param) { let settings = self.wake_times.entry(bulb.clone()).or_default(); let Param::String(time) = time else { error!("invalit param kind"); return; }; let time = NaiveTime::parse_from_str(&time, TIME_FMT) .map(Some) .unwrap_or(None); let weekday = match name { "Mon" => Weekday::Mon, "Tue" => Weekday::Tue, "Wed" => Weekday::Wed, "Thu" => Weekday::Thu, "Fri" => Weekday::Fri, "Sat" => Weekday::Sat, "Sun" => Weekday::Sun, _ => { error!("invalit param name"); return; } }; let Some(time) = time else { settings.remove(&weekday); self.wake_tasks.remove(&(bulb.clone(), weekday)); return; }; settings.insert(weekday, time); let task = spawn(wake_task(self.manager.clone(), bulb.clone(), weekday, time)); self.wake_tasks .insert((bulb.clone(), weekday), DeadMansHandle(task.abort_handle())); } } const DAYS_OF_WEEK: &[Weekday] = &[ Weekday::Mon, Weekday::Tue, Weekday::Wed, Weekday::Thu, Weekday::Fri, Weekday::Sat, Weekday::Sun, ]; async fn wake_task(manager: BulbManager, id: BulbId, day: Weekday, time: NaiveTime) { let mut alarm = next_alarm(Local::now(), day, time); loop { info!("waking lamp {id:?} at {alarm}"); sleep((alarm - Local::now()).to_std().unwrap()).await; if let Some(bulb) = manager.bulbs().await.get(&id) { // don't wake the bulb if it's already turned on if bulb.power { continue; } } else { warn!("bulb {id:?} does not exist"); return; }; info!("waking lamp {id:?}"); let r = manager .until_interrupted(id.clone(), async { // slowly turn up brightness of bulb for brightness in (1..=75).map(|i| (i as f32) * 0.01) { //sleep(Duration::from_secs(12)).await; sleep(Duration::from_millis(500)).await; manager .send_command(BulbCommand::SetColor( BulbSelector::Id(id.clone()), BulbColor::Kelvin { t: 0.0, b: brightness, }, )) .await } }) .await; if r.is_none() { info!("interrupted waking lamp {id:?}"); } alarm = next_alarm(Local::now(), day, time); } } /// Get the next alarm, from a weekday+time schedule. fn next_alarm(now: DateTime, day: Weekday, time: NaiveTime) -> DateTime { let day_of_alarm = day.num_days_from_monday() as i64; let day_now = now.weekday().num_days_from_monday() as i64; let alarm = now + chrono::Duration::days(day_of_alarm - day_now); let mut alarm = alarm .date_naive() .and_time(time) .and_local_timezone(Local) .unwrap(); if alarm <= now { alarm += chrono::Duration::weeks(1); } alarm } #[cfg(test)] mod test { use super::next_alarm; use chrono::{offset::TimeZone, Local, NaiveTime, Weekday}; #[test] fn test_alarm_date() { const FMT: &str = "%Y-%m-%d %H:%M"; let now = Local.datetime_from_str("2022-10-18 15:30", FMT).unwrap(); let test_values = [ (Weekday::Tue, (16, 30), "2022-10-18 16:30"), (Weekday::Tue, (14, 30), "2022-10-25 14:30"), (Weekday::Wed, (15, 30), "2022-10-19 15:30"), (Weekday::Mon, (15, 30), "2022-10-24 15:30"), ]; for (day, (hour, min), expected) in test_values { let expected = Local.datetime_from_str(expected, FMT).unwrap(); assert_eq!( next_alarm(now, day, NaiveTime::from_hms(hour, min, 0)), expected ); } } }