Compare commits
No commits in common. 'master' and 'drone-volume-cache' have entirely different histories.
master
...
drone-volu
62 changed files with 697 additions and 5419 deletions
@ -1,14 +1,4 @@ |
|||||||
# Cargo/Rust |
|
||||||
/target |
/target |
||||||
Cargo.lock |
/.vscode |
||||||
|
|
||||||
# Visual studio code |
|
||||||
.vscode |
|
||||||
|
|
||||||
# Sqlite databases |
|
||||||
/*.db |
/*.db |
||||||
*.db-shm |
Cargo.lock |
||||||
*.db-wal |
|
||||||
|
|
||||||
# Config file |
|
||||||
/sprinklers_rs.json |
|
@ -1,10 +1,22 @@ |
|||||||
[workspace] |
[package] |
||||||
|
name = "sprinklers_rs" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
||||||
|
edition = "2018" |
||||||
|
|
||||||
members = [ |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||||
"sprinklers_core", |
|
||||||
"sprinklers_database", |
[dependencies] |
||||||
"sprinklers_actors", |
rusqlite = "0.23.1" |
||||||
"sprinklers_mqtt", |
color-eyre = "0.5.1" |
||||||
"sprinklers_linux", |
eyre = "0.6.0" |
||||||
"sprinklers_rs" |
thiserror = "1.0.20" |
||||||
] |
tokio = { version = "0.2.22", features = ["rt-core", "time", "sync", "macros", "test-util"] } |
||||||
|
tracing = { version = "0.1.19", features = ["log"] } |
||||||
|
tracing-futures = "0.2.4" |
||||||
|
pin-project = "0.4.23" |
||||||
|
|
||||||
|
[dependencies.tracing-subscriber] |
||||||
|
version = "0.2.11" |
||||||
|
default-features = false |
||||||
|
features = ["registry", "fmt", "env-filter", "ansi"] |
||||||
|
@ -1,37 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_actors" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
sprinklers_core = { path = "../sprinklers_core" } |
|
||||||
actix = { version = "0.10.0", default-features = false } |
|
||||||
thiserror = "1.0.20" |
|
||||||
tracing = "0.1.19" |
|
||||||
chrono = { version = "0.4.15" } |
|
||||||
serde = { version = "1.0.116", features = ["derive"] } |
|
||||||
im = "15.0.0" |
|
||||||
eyre = "0.6.0" |
|
||||||
|
|
||||||
[dependencies.tokio] |
|
||||||
version = "0.2.22" |
|
||||||
default-features = false |
|
||||||
features = [] |
|
||||||
|
|
||||||
[dependencies.futures-util] |
|
||||||
version = "0.3.5" |
|
||||||
default-features = false |
|
||||||
features = ["std", "async-await", "sink"] |
|
||||||
|
|
||||||
[dev-dependencies] |
|
||||||
actix-rt = "1.1.1" |
|
||||||
tokio = { version = "0.2.22", features = ["test-util"] } |
|
||||||
assert_matches = "1.3.0" |
|
||||||
|
|
||||||
[dev-dependencies.tracing-subscriber] |
|
||||||
version = "0.2.11" |
|
||||||
default-features = false |
|
||||||
features = ["registry"] |
|
@ -1,10 +0,0 @@ |
|||||||
pub mod program_runner; |
|
||||||
pub mod state_manager; |
|
||||||
pub mod zone_runner; |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod trace_listeners; |
|
||||||
|
|
||||||
pub use program_runner::ProgramRunner; |
|
||||||
pub use state_manager::StateManager; |
|
||||||
pub use zone_runner::ZoneRunner; |
|
File diff suppressed because it is too large
Load Diff
@ -1,72 +0,0 @@ |
|||||||
use sprinklers_core::model::{ProgramId, ProgramRef, ProgramUpdateData, Programs, Zones}; |
|
||||||
use thiserror::Error; |
|
||||||
use tokio::sync::{mpsc, oneshot, watch}; |
|
||||||
|
|
||||||
#[derive(Debug)] |
|
||||||
pub enum Request { |
|
||||||
UpdateProgram { |
|
||||||
id: ProgramId, |
|
||||||
update: ProgramUpdateData, |
|
||||||
resp_tx: oneshot::Sender<Result<ProgramRef>>, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
pub struct StateManager { |
|
||||||
request_tx: mpsc::Sender<Request>, |
|
||||||
zones_watch: watch::Receiver<Zones>, |
|
||||||
programs_watch: watch::Receiver<Programs>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Error)] |
|
||||||
pub enum StateError { |
|
||||||
#[error("no such program: {0}")] |
|
||||||
NoSuchProgram(ProgramId), |
|
||||||
#[error("internal error: {0}")] |
|
||||||
Other( |
|
||||||
#[from] |
|
||||||
#[source] |
|
||||||
eyre::Report, |
|
||||||
), |
|
||||||
} |
|
||||||
|
|
||||||
pub type Result<T, E = StateError> = std::result::Result<T, E>; |
|
||||||
|
|
||||||
impl StateManager { |
|
||||||
pub fn new( |
|
||||||
request_tx: mpsc::Sender<Request>, |
|
||||||
zones_watch: watch::Receiver<Zones>, |
|
||||||
programs_watch: watch::Receiver<Programs>, |
|
||||||
) -> Self { |
|
||||||
Self { |
|
||||||
request_tx, |
|
||||||
zones_watch, |
|
||||||
programs_watch, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn update_program( |
|
||||||
&mut self, |
|
||||||
id: ProgramId, |
|
||||||
update: ProgramUpdateData, |
|
||||||
) -> Result<ProgramRef> { |
|
||||||
let (resp_tx, resp_rx) = oneshot::channel(); |
|
||||||
self.request_tx |
|
||||||
.send(Request::UpdateProgram { |
|
||||||
id, |
|
||||||
update, |
|
||||||
resp_tx, |
|
||||||
}) |
|
||||||
.await |
|
||||||
.map_err(eyre::Report::from)?; |
|
||||||
resp_rx.await.map_err(eyre::Report::from)? |
|
||||||
} |
|
||||||
|
|
||||||
pub fn get_zones(&self) -> watch::Receiver<Zones> { |
|
||||||
self.zones_watch.clone() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn get_programs(&self) -> watch::Receiver<Programs> { |
|
||||||
self.programs_watch.clone() |
|
||||||
} |
|
||||||
} |
|
@ -1,891 +0,0 @@ |
|||||||
use sprinklers_core::model::{ZoneId, ZoneRef}; |
|
||||||
use sprinklers_core::zone_interface::ZoneInterface; |
|
||||||
|
|
||||||
use actix::{ |
|
||||||
Actor, ActorContext, Addr, AsyncContext, Handler, Message, MessageResult, SpawnHandle, |
|
||||||
}; |
|
||||||
use futures_util::TryFutureExt; |
|
||||||
use std::{ |
|
||||||
future::Future, |
|
||||||
mem::swap, |
|
||||||
sync::{ |
|
||||||
atomic::{AtomicI32, Ordering}, |
|
||||||
Arc, |
|
||||||
}, |
|
||||||
time::Duration, |
|
||||||
}; |
|
||||||
use thiserror::Error; |
|
||||||
use tokio::{ |
|
||||||
sync::{broadcast, watch}, |
|
||||||
time::Instant, |
|
||||||
}; |
|
||||||
use tracing::{debug, trace, warn}; |
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] |
|
||||||
pub struct ZoneRunHandle(i32); |
|
||||||
|
|
||||||
impl ZoneRunHandle { |
|
||||||
pub fn into_inner(self) -> i32 { |
|
||||||
self.0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub enum ZoneEvent { |
|
||||||
RunStart(ZoneRunHandle, ZoneRef), |
|
||||||
RunFinish(ZoneRunHandle, ZoneRef), |
|
||||||
RunPause(ZoneRunHandle, ZoneRef), |
|
||||||
RunUnpause(ZoneRunHandle, ZoneRef), |
|
||||||
RunCancel(ZoneRunHandle, ZoneRef), |
|
||||||
RunnerPause, |
|
||||||
RunnerUnpause, |
|
||||||
} |
|
||||||
|
|
||||||
pub type ZoneEventRecv = broadcast::Receiver<ZoneEvent>; |
|
||||||
type ZoneEventSend = broadcast::Sender<ZoneEvent>; |
|
||||||
|
|
||||||
const EVENT_CAPACITY: usize = 8; |
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)] |
|
||||||
pub enum ZoneRunState { |
|
||||||
Waiting, |
|
||||||
Running { |
|
||||||
start_time: Instant, |
|
||||||
}, |
|
||||||
Finished, |
|
||||||
Cancelled, |
|
||||||
Paused { |
|
||||||
start_time: Instant, |
|
||||||
pause_time: Instant, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct ZoneRun { |
|
||||||
pub handle: ZoneRunHandle, |
|
||||||
pub zone: ZoneRef, |
|
||||||
pub duration: Duration, |
|
||||||
pub total_duration: Duration, |
|
||||||
pub state: ZoneRunState, |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneRun { |
|
||||||
fn new(handle: ZoneRunHandle, zone: ZoneRef, duration: Duration) -> Self { |
|
||||||
Self { |
|
||||||
handle, |
|
||||||
zone, |
|
||||||
duration, |
|
||||||
total_duration: duration, |
|
||||||
state: ZoneRunState::Waiting, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn is_running(&self) -> bool { |
|
||||||
matches!(self.state, ZoneRunState::Running{..}) |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(dead_code)] |
|
||||||
pub fn is_paused(&self) -> bool { |
|
||||||
matches!(self.state, ZoneRunState::Paused{..}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub type ZoneRunQueue = im::Vector<Arc<ZoneRun>>; |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct ZoneRunnerState { |
|
||||||
pub run_queue: ZoneRunQueue, |
|
||||||
pub paused: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for ZoneRunnerState { |
|
||||||
fn default() -> Self { |
|
||||||
Self { |
|
||||||
run_queue: ZoneRunQueue::default(), |
|
||||||
paused: false, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub type ZoneRunnerStateRecv = watch::Receiver<ZoneRunnerState>; |
|
||||||
|
|
||||||
struct ZoneRunnerInner { |
|
||||||
interface: Arc<dyn ZoneInterface>, |
|
||||||
event_send: Option<ZoneEventSend>, |
|
||||||
state_send: watch::Sender<ZoneRunnerState>, |
|
||||||
delay_future: Option<SpawnHandle>, |
|
||||||
did_change: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneRunnerInner { |
|
||||||
fn send_event(&mut self, event: ZoneEvent) { |
|
||||||
if let Some(event_send) = &mut self.event_send { |
|
||||||
match event_send.send(event) { |
|
||||||
Ok(_) => {} |
|
||||||
Err(_closed) => { |
|
||||||
self.event_send = None; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn subscribe_event(&mut self) -> ZoneEventRecv { |
|
||||||
match &mut self.event_send { |
|
||||||
Some(event_send) => event_send.subscribe(), |
|
||||||
None => { |
|
||||||
let (event_send, event_recv) = broadcast::channel(EVENT_CAPACITY); |
|
||||||
self.event_send = Some(event_send); |
|
||||||
event_recv |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn start_run(&mut self, run: &mut Arc<ZoneRun>) { |
|
||||||
use ZoneRunState::*; |
|
||||||
let run = Arc::make_mut(run); |
|
||||||
debug!(zone_id = run.zone.id, "starting running zone"); |
|
||||||
self.interface.set_zone_state(run.zone.interface_id, true); |
|
||||||
run.state = Running { |
|
||||||
start_time: Instant::now(), |
|
||||||
}; |
|
||||||
self.send_event(ZoneEvent::RunStart(run.handle.clone(), run.zone.clone())); |
|
||||||
self.did_change = true; |
|
||||||
} |
|
||||||
|
|
||||||
fn finish_run(&mut self, run: &mut Arc<ZoneRun>) { |
|
||||||
let run = Arc::make_mut(run); |
|
||||||
if run.is_running() { |
|
||||||
debug!(zone_id = run.zone.id, "finished running zone"); |
|
||||||
self.interface.set_zone_state(run.zone.interface_id, false); |
|
||||||
run.state = ZoneRunState::Finished; |
|
||||||
self.send_event(ZoneEvent::RunFinish(run.handle.clone(), run.zone.clone())); |
|
||||||
self.did_change = true; |
|
||||||
} else { |
|
||||||
warn!( |
|
||||||
zone_id = run.zone.id, |
|
||||||
state = debug(&run.state), |
|
||||||
"cannot finish run which is not running" |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn cancel_run(&mut self, run: &mut Arc<ZoneRun>) -> bool { |
|
||||||
let run = Arc::make_mut(run); |
|
||||||
if run.is_running() { |
|
||||||
debug!(zone_id = run.zone.id, "cancelling running zone"); |
|
||||||
self.interface.set_zone_state(run.zone.interface_id, false); |
|
||||||
} |
|
||||||
if run.state != ZoneRunState::Cancelled { |
|
||||||
run.state = ZoneRunState::Cancelled; |
|
||||||
self.send_event(ZoneEvent::RunCancel(run.handle.clone(), run.zone.clone())); |
|
||||||
self.did_change = true; |
|
||||||
true |
|
||||||
} else { |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn pause_run(&mut self, run: &mut Arc<ZoneRun>) { |
|
||||||
use ZoneRunState::*; |
|
||||||
let run = Arc::make_mut(run); |
|
||||||
let new_state = match run.state { |
|
||||||
Running { start_time } => { |
|
||||||
debug!(zone_id = run.zone.id, "pausing running zone"); |
|
||||||
self.interface.set_zone_state(run.zone.interface_id, false); |
|
||||||
Paused { |
|
||||||
start_time, |
|
||||||
pause_time: Instant::now(), |
|
||||||
} |
|
||||||
} |
|
||||||
Waiting => { |
|
||||||
debug!(zone_id = run.zone.id, "pausing waiting zone"); |
|
||||||
Paused { |
|
||||||
start_time: Instant::now(), |
|
||||||
pause_time: Instant::now(), |
|
||||||
} |
|
||||||
} |
|
||||||
Finished | Cancelled | Paused { .. } => { |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
run.state = new_state; |
|
||||||
self.send_event(ZoneEvent::RunPause(run.handle.clone(), run.zone.clone())); |
|
||||||
self.did_change = true; |
|
||||||
} |
|
||||||
|
|
||||||
fn unpause_run(&mut self, run: &mut Arc<ZoneRun>) { |
|
||||||
use ZoneRunState::*; |
|
||||||
let run = Arc::make_mut(run); |
|
||||||
match run.state { |
|
||||||
Paused { |
|
||||||
start_time, |
|
||||||
pause_time, |
|
||||||
} => { |
|
||||||
debug!(zone_id = run.zone.id, "unpausing zone"); |
|
||||||
self.interface.set_zone_state(run.zone.interface_id, true); |
|
||||||
run.state = Running { |
|
||||||
start_time: Instant::now(), |
|
||||||
}; |
|
||||||
let ran_for = pause_time - start_time; |
|
||||||
run.duration -= ran_for; |
|
||||||
self.send_event(ZoneEvent::RunUnpause(run.handle.clone(), run.zone.clone())); |
|
||||||
} |
|
||||||
Waiting | Finished | Cancelled | Running { .. } => { |
|
||||||
warn!( |
|
||||||
zone_id = run.zone.id, |
|
||||||
state = debug(&run.state), |
|
||||||
"can only unpause paused zone" |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
self.did_change = true; |
|
||||||
} |
|
||||||
|
|
||||||
fn process_after_delay( |
|
||||||
&mut self, |
|
||||||
after: Duration, |
|
||||||
ctx: &mut <ZoneRunnerActor as Actor>::Context, |
|
||||||
) { |
|
||||||
let delay_future = ctx.notify_later(Process, after); |
|
||||||
if let Some(old_future) = self.delay_future.replace(delay_future) { |
|
||||||
ctx.cancel_future(old_future); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn cancel_process(&mut self, ctx: &mut <ZoneRunnerActor as Actor>::Context) { |
|
||||||
if let Some(old_future) = self.delay_future.take() { |
|
||||||
ctx.cancel_future(old_future); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
struct ZoneRunnerActor { |
|
||||||
state: ZoneRunnerState, |
|
||||||
inner: ZoneRunnerInner, |
|
||||||
} |
|
||||||
|
|
||||||
impl Actor for ZoneRunnerActor { |
|
||||||
type Context = actix::Context<Self>; |
|
||||||
|
|
||||||
fn started(&mut self, _ctx: &mut Self::Context) { |
|
||||||
trace!("zone_runner starting"); |
|
||||||
for i in 0..self.inner.interface.num_zones() { |
|
||||||
self.inner.interface.set_zone_state(i, false); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn stopped(&mut self, _ctx: &mut Self::Context) { |
|
||||||
trace!("zone_runner stopped"); |
|
||||||
for i in 0..self.inner.interface.num_zones() { |
|
||||||
self.inner.interface.set_zone_state(i, false); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct Quit; |
|
||||||
|
|
||||||
impl Handler<Quit> for ZoneRunnerActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Quit, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
ctx.stop(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct QueueRun(ZoneRunHandle, ZoneRef, Duration); |
|
||||||
|
|
||||||
impl Handler<QueueRun> for ZoneRunnerActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, msg: QueueRun, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let QueueRun(handle, zone, duration) = msg; |
|
||||||
|
|
||||||
let run: Arc<ZoneRun> = ZoneRun::new(handle, zone, duration).into(); |
|
||||||
self.state.run_queue.push_back(run); |
|
||||||
self.inner.did_change = true; |
|
||||||
|
|
||||||
ctx.notify(Process); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "bool")] |
|
||||||
struct CancelRun(ZoneRunHandle); |
|
||||||
|
|
||||||
impl Handler<CancelRun> for ZoneRunnerActor { |
|
||||||
type Result = bool; |
|
||||||
|
|
||||||
fn handle(&mut self, msg: CancelRun, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let CancelRun(handle) = msg; |
|
||||||
let mut cancelled = false; |
|
||||||
for run in self |
|
||||||
.state |
|
||||||
.run_queue |
|
||||||
.iter_mut() |
|
||||||
.filter(|run| run.handle == handle) |
|
||||||
{ |
|
||||||
trace!(handle = handle.0, "cancelling run by handle"); |
|
||||||
cancelled = self.inner.cancel_run(run); |
|
||||||
} |
|
||||||
ctx.notify(Process); |
|
||||||
cancelled |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "usize")] |
|
||||||
struct CancelByZone(ZoneId); |
|
||||||
|
|
||||||
impl Handler<CancelByZone> for ZoneRunnerActor { |
|
||||||
type Result = usize; |
|
||||||
|
|
||||||
fn handle(&mut self, msg: CancelByZone, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let CancelByZone(zone_id) = msg; |
|
||||||
let mut count = 0_usize; |
|
||||||
for run in self |
|
||||||
.state |
|
||||||
.run_queue |
|
||||||
.iter_mut() |
|
||||||
.filter(|run| run.zone.id == zone_id) |
|
||||||
{ |
|
||||||
trace!(handle = run.handle.0, zone_id, "cancelling run by zone"); |
|
||||||
if self.inner.cancel_run(run) { |
|
||||||
count += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
ctx.notify(Process); |
|
||||||
count |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "usize")] |
|
||||||
struct CancelAll; |
|
||||||
|
|
||||||
impl Handler<CancelAll> for ZoneRunnerActor { |
|
||||||
type Result = usize; |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: CancelAll, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let mut old_runs = ZoneRunQueue::new(); |
|
||||||
swap(&mut old_runs, &mut self.state.run_queue); |
|
||||||
trace!(count = old_runs.len(), "cancelling all runs"); |
|
||||||
let mut count = 0usize; |
|
||||||
for mut run in old_runs { |
|
||||||
if self.inner.cancel_run(&mut run) { |
|
||||||
count += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
ctx.notify(Process); |
|
||||||
count |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct SetPaused(bool); |
|
||||||
|
|
||||||
impl Handler<SetPaused> for ZoneRunnerActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, msg: SetPaused, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let SetPaused(pause) = msg; |
|
||||||
if pause != self.state.paused { |
|
||||||
if pause { |
|
||||||
self.state.paused = true; |
|
||||||
self.inner.send_event(ZoneEvent::RunnerPause); |
|
||||||
} else { |
|
||||||
self.state.paused = false; |
|
||||||
self.inner.send_event(ZoneEvent::RunnerUnpause); |
|
||||||
} |
|
||||||
self.inner.did_change = true; |
|
||||||
ctx.notify(Process); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "ZoneEventRecv")] |
|
||||||
struct Subscribe; |
|
||||||
|
|
||||||
impl Handler<Subscribe> for ZoneRunnerActor { |
|
||||||
type Result = MessageResult<Subscribe>; |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Subscribe, _ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let event_recv = self.inner.subscribe_event(); |
|
||||||
MessageResult(event_recv) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct Process; |
|
||||||
|
|
||||||
impl Handler<Process> for ZoneRunnerActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Process, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
self.process(ctx) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneRunnerActor { |
|
||||||
fn new(interface: Arc<dyn ZoneInterface>, state_send: watch::Sender<ZoneRunnerState>) -> Self { |
|
||||||
Self { |
|
||||||
state: ZoneRunnerState::default(), |
|
||||||
inner: ZoneRunnerInner { |
|
||||||
interface, |
|
||||||
event_send: None, |
|
||||||
state_send, |
|
||||||
delay_future: None, |
|
||||||
did_change: false, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn process_queue(&mut self, ctx: &mut actix::Context<Self>) { |
|
||||||
use ZoneRunState::*; |
|
||||||
let state = &mut self.state; |
|
||||||
while let Some(current_run) = state.run_queue.front_mut() { |
|
||||||
let run_finished = match (¤t_run.state, state.paused) { |
|
||||||
(Waiting, false) => { |
|
||||||
self.inner.start_run(current_run); |
|
||||||
self.inner.process_after_delay(current_run.duration, ctx); |
|
||||||
false |
|
||||||
} |
|
||||||
(Running { start_time }, false) => { |
|
||||||
let time_to_finish = start_time.elapsed() >= current_run.duration; |
|
||||||
if time_to_finish { |
|
||||||
self.inner.finish_run(current_run); |
|
||||||
self.inner.cancel_process(ctx); |
|
||||||
} |
|
||||||
time_to_finish |
|
||||||
} |
|
||||||
(Paused { .. }, false) => { |
|
||||||
self.inner.unpause_run(current_run); |
|
||||||
self.inner.process_after_delay(current_run.duration, ctx); |
|
||||||
false |
|
||||||
} |
|
||||||
(Waiting, true) => { |
|
||||||
self.inner.pause_run(current_run); |
|
||||||
false |
|
||||||
} |
|
||||||
(Running { .. }, true) => { |
|
||||||
self.inner.pause_run(current_run); |
|
||||||
self.inner.cancel_process(ctx); |
|
||||||
false |
|
||||||
} |
|
||||||
(Paused { .. }, true) => false, |
|
||||||
(Cancelled, _) | (Finished, _) => true, |
|
||||||
}; |
|
||||||
|
|
||||||
if run_finished { |
|
||||||
state.run_queue.pop_front(); |
|
||||||
} else { |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn process(&mut self, ctx: &mut actix::Context<Self>) { |
|
||||||
self.process_queue(ctx); |
|
||||||
|
|
||||||
if self.inner.did_change { |
|
||||||
let _ = self.inner.state_send.broadcast(self.state.clone()); |
|
||||||
self.inner.did_change = false; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)] |
|
||||||
#[error("error communicating with ZoneRunner: {0}")] |
|
||||||
pub struct Error(#[from] actix::MailboxError); |
|
||||||
|
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>; |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
pub struct ZoneRunner { |
|
||||||
state_recv: ZoneRunnerStateRecv, |
|
||||||
addr: Addr<ZoneRunnerActor>, |
|
||||||
next_run_id: Arc<AtomicI32>, |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(dead_code)] |
|
||||||
impl ZoneRunner { |
|
||||||
pub fn new(interface: Arc<dyn ZoneInterface>) -> Self { |
|
||||||
let (state_send, state_recv) = watch::channel(ZoneRunnerState::default()); |
|
||||||
let addr = ZoneRunnerActor::new(interface, state_send).start(); |
|
||||||
Self { |
|
||||||
state_recv, |
|
||||||
addr, |
|
||||||
next_run_id: Arc::new(AtomicI32::new(1)), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn quit(&mut self) -> impl Future<Output = Result<()>> { |
|
||||||
self.addr.send(Quit).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
fn queue_run_inner(&mut self, zone: ZoneRef, duration: Duration) -> (QueueRun, ZoneRunHandle) { |
|
||||||
let run_id = self.next_run_id.fetch_add(1, Ordering::SeqCst); |
|
||||||
let handle = ZoneRunHandle(run_id); |
|
||||||
(QueueRun(handle.clone(), zone, duration), handle) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn do_queue_run(&mut self, zone: ZoneRef, duration: Duration) -> ZoneRunHandle { |
|
||||||
let (queue_run, handle) = self.queue_run_inner(zone, duration); |
|
||||||
self.addr.do_send(queue_run); |
|
||||||
handle |
|
||||||
} |
|
||||||
|
|
||||||
pub fn queue_run( |
|
||||||
&mut self, |
|
||||||
zone: ZoneRef, |
|
||||||
duration: Duration, |
|
||||||
) -> impl Future<Output = Result<ZoneRunHandle>> { |
|
||||||
let (queue_run, handle) = self.queue_run_inner(zone, duration); |
|
||||||
self.addr |
|
||||||
.send(queue_run) |
|
||||||
.map_err(From::from) |
|
||||||
.map_ok(move |_| handle) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn do_cancel_run(&mut self, handle: ZoneRunHandle) { |
|
||||||
self.addr.do_send(CancelRun(handle)) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn cancel_run(&mut self, handle: ZoneRunHandle) -> impl Future<Output = Result<bool>> { |
|
||||||
self.addr.send(CancelRun(handle)).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn cancel_by_zone(&mut self, zone_id: ZoneId) -> impl Future<Output = Result<usize>> { |
|
||||||
self.addr.send(CancelByZone(zone_id)).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn cancel_all(&mut self) -> impl Future<Output = Result<usize>> { |
|
||||||
self.addr.send(CancelAll).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn pause(&mut self) -> impl Future<Output = Result<()>> { |
|
||||||
self.addr.send(SetPaused(true)).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn unpause(&mut self) -> impl Future<Output = Result<()>> { |
|
||||||
self.addr.send(SetPaused(false)).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn subscribe(&mut self) -> impl Future<Output = Result<ZoneEventRecv>> { |
|
||||||
self.addr.send(Subscribe).map_err(From::from) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn get_state_recv(&self) -> ZoneRunnerStateRecv { |
|
||||||
self.state_recv.clone() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod test { |
|
||||||
use super::*; |
|
||||||
use crate::trace_listeners::{EventListener, Filters}; |
|
||||||
use sprinklers_core::{ |
|
||||||
model::{Zone, Zones}, |
|
||||||
zone_interface::MockZoneInterface, |
|
||||||
}; |
|
||||||
|
|
||||||
use assert_matches::assert_matches; |
|
||||||
use im::ordmap; |
|
||||||
use tracing_subscriber::prelude::*; |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_quit() { |
|
||||||
let quit_msg = EventListener::new( |
|
||||||
Filters::new() |
|
||||||
.target("sprinklers_actors::zone_runner") |
|
||||||
.message("zone_runner stopped"), |
|
||||||
); |
|
||||||
let subscriber = tracing_subscriber::registry().with(quit_msg.clone()); |
|
||||||
let _sub = tracing::subscriber::set_default(subscriber); |
|
||||||
|
|
||||||
let interface = MockZoneInterface::new(6); |
|
||||||
let mut runner = ZoneRunner::new(Arc::new(interface)); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
runner.quit().await.unwrap(); |
|
||||||
|
|
||||||
assert_eq!(quit_msg.get_count(), 1); |
|
||||||
} |
|
||||||
|
|
||||||
fn make_zones_and_interface() -> (Zones, Arc<MockZoneInterface>) { |
|
||||||
let interface = Arc::new(MockZoneInterface::new(2)); |
|
||||||
let zones: Zones = ordmap![ |
|
||||||
1 => Zone { |
|
||||||
id: 1, |
|
||||||
name: "Zone 1".into(), |
|
||||||
interface_id: 0, |
|
||||||
}.into(), |
|
||||||
2 => Zone { |
|
||||||
id: 2, |
|
||||||
name: "Zone 2".into(), |
|
||||||
interface_id: 1, |
|
||||||
}.into() |
|
||||||
]; |
|
||||||
(zones, interface) |
|
||||||
} |
|
||||||
|
|
||||||
fn assert_zone_states(interface: &MockZoneInterface, states: &[bool]) { |
|
||||||
for (id, state) in states.iter().enumerate() { |
|
||||||
assert_eq!( |
|
||||||
interface.get_zone_state(id as u32), |
|
||||||
*state, |
|
||||||
"zone interface id {} did not match", |
|
||||||
id |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async fn advance(dur: Duration) { |
|
||||||
// HACK: advance should really be enough, but we need another yield_now
|
|
||||||
tokio::time::pause(); |
|
||||||
tokio::time::advance(dur).await; |
|
||||||
tokio::task::yield_now().await; |
|
||||||
tokio::time::resume(); |
|
||||||
} |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_queue() { |
|
||||||
let (zones, interface) = make_zones_and_interface(); |
|
||||||
let mut runner = ZoneRunner::new(interface.clone()); |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
// Queue single zone, make sure it runs
|
|
||||||
runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
tokio::task::yield_now().await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[true, false]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
// Queue two zones, make sure they run one at a time
|
|
||||||
runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
tokio::task::yield_now().await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[true, false]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(10)).await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.quit().await.unwrap(); |
|
||||||
} |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_cancel_run() { |
|
||||||
let (zones, interface) = make_zones_and_interface(); |
|
||||||
let mut runner = ZoneRunner::new(interface.clone()); |
|
||||||
|
|
||||||
let run1 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let _run2 = runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let run3 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
tokio::task::yield_now().await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
runner.cancel_run(run1).await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[true, false]); |
|
||||||
|
|
||||||
runner.cancel_run(run3).await.unwrap(); |
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
|
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.quit().await.unwrap(); |
|
||||||
} |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_cancel_all() { |
|
||||||
let (zones, interface) = make_zones_and_interface(); |
|
||||||
let mut runner = ZoneRunner::new(interface.clone()); |
|
||||||
|
|
||||||
runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
runner.cancel_all().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.cancel_all().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.quit().await.unwrap(); |
|
||||||
} |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_pause() { |
|
||||||
let (zones, interface) = make_zones_and_interface(); |
|
||||||
let mut runner = ZoneRunner::new(interface.clone()); |
|
||||||
|
|
||||||
let _run1 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let run2 = runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let _run3 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
runner.pause().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(10)).await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.unpause().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(8)).await; |
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(2)).await; |
|
||||||
assert_zone_states(&interface, &[true, false]); |
|
||||||
|
|
||||||
runner.pause().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
// cancel paused run
|
|
||||||
runner.cancel_run(run2).await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.unpause().await.unwrap(); |
|
||||||
tokio::task::yield_now().await; |
|
||||||
assert_zone_states(&interface, &[false, true]); |
|
||||||
|
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
assert_zone_states(&interface, &[false, false]); |
|
||||||
|
|
||||||
runner.quit().await.unwrap(); |
|
||||||
} |
|
||||||
|
|
||||||
#[actix_rt::test] |
|
||||||
async fn test_event() { |
|
||||||
let (zones, interface) = make_zones_and_interface(); |
|
||||||
let mut runner = ZoneRunner::new(interface.clone()); |
|
||||||
|
|
||||||
let mut event_recv = runner.subscribe().await.unwrap(); |
|
||||||
|
|
||||||
let run1 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let run2 = runner |
|
||||||
.queue_run(zones[&1].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
let run3 = runner |
|
||||||
.queue_run(zones[&2].clone(), Duration::from_secs(10)) |
|
||||||
.await |
|
||||||
.unwrap(); |
|
||||||
|
|
||||||
assert_matches!( |
|
||||||
event_recv.recv().await, |
|
||||||
Ok(ZoneEvent::RunStart(handle, _)) |
|
||||||
if handle == run1 |
|
||||||
); |
|
||||||
|
|
||||||
runner.pause().await.unwrap(); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunnerPause)); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunPause(handle, _)) if handle == run1); |
|
||||||
|
|
||||||
runner.unpause().await.unwrap(); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunnerUnpause)); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunUnpause(handle, _)) if handle == run1); |
|
||||||
|
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunFinish(handle, _)) if handle == run1); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunStart(handle, _)) if handle == run2); |
|
||||||
|
|
||||||
runner.pause().await.unwrap(); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunnerPause)); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunPause(handle, _)) if handle == run2); |
|
||||||
|
|
||||||
// cancel paused run
|
|
||||||
runner.cancel_run(run2.clone()).await.unwrap(); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunCancel(handle, _)) if handle == run2); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunPause(handle, _)) if handle == run3); |
|
||||||
|
|
||||||
runner.unpause().await.unwrap(); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunnerUnpause)); |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunUnpause(handle, _)) if handle == run3); |
|
||||||
|
|
||||||
advance(Duration::from_secs(11)).await; |
|
||||||
assert_matches!(event_recv.recv().await, Ok(ZoneEvent::RunFinish(handle, _)) if handle == run3); |
|
||||||
|
|
||||||
runner.quit().await.unwrap(); |
|
||||||
} |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_core" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
chrono = { version = "0.4.15" } |
|
||||||
serde = { version = "1.0.116", features = ["derive"] } |
|
||||||
im = "15.0.0" |
|
||||||
tracing = { version = "0.1.19" } |
|
||||||
|
|
||||||
[dev-dependencies] |
|
||||||
serde_json = "1.0.57" |
|
@ -1,4 +0,0 @@ |
|||||||
pub mod model; |
|
||||||
pub mod schedule; |
|
||||||
pub mod serde; |
|
||||||
pub mod zone_interface; |
|
@ -1,7 +0,0 @@ |
|||||||
//! Domain specific data models
|
|
||||||
|
|
||||||
mod program; |
|
||||||
mod zone; |
|
||||||
|
|
||||||
pub use program::*; |
|
||||||
pub use zone::*; |
|
@ -1,41 +0,0 @@ |
|||||||
use super::zone::ZoneId; |
|
||||||
use crate::schedule::Schedule; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::{sync::Arc, time::Duration}; |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct ProgramItem { |
|
||||||
// TODO: update nomenclature
|
|
||||||
#[serde(rename = "sectionId")] |
|
||||||
pub zone_id: ZoneId, |
|
||||||
#[serde(with = "crate::serde::duration_secs")] |
|
||||||
pub duration: Duration, |
|
||||||
} |
|
||||||
|
|
||||||
pub type ProgramSequence = Vec<ProgramItem>; |
|
||||||
|
|
||||||
pub type ProgramId = u32; |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct Program { |
|
||||||
pub id: ProgramId, |
|
||||||
pub name: String, |
|
||||||
pub sequence: ProgramSequence, |
|
||||||
pub enabled: bool, |
|
||||||
pub schedule: Schedule, |
|
||||||
} |
|
||||||
|
|
||||||
pub type ProgramRef = Arc<Program>; |
|
||||||
|
|
||||||
pub type Programs = im::OrdMap<ProgramId, ProgramRef>; |
|
||||||
|
|
||||||
#[derive(Default, Debug, Serialize, Deserialize)] |
|
||||||
#[serde(default, rename_all = "camelCase")] |
|
||||||
pub struct ProgramUpdateData { |
|
||||||
pub name: Option<String>, |
|
||||||
pub sequence: Option<ProgramSequence>, |
|
||||||
pub enabled: Option<bool>, |
|
||||||
pub schedule: Option<Schedule>, |
|
||||||
} |
|
@ -1,27 +0,0 @@ |
|||||||
//! Data models for sprinklers zones
|
|
||||||
//!
|
|
||||||
//! A zone represents a group of sprinkler heads actuated by a single
|
|
||||||
//! valve. Physically controllable (or virtual) valves are handled by implementations of
|
|
||||||
//! [ZoneInterface](../../zone_interface/trait.ZoneInterface.html), but the model
|
|
||||||
//! describes a logical zone and how it maps to a physical one.
|
|
||||||
|
|
||||||
use crate::zone_interface::ZoneNum; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
/// Identifying integer type for a Zone
|
|
||||||
pub type ZoneId = u32; |
|
||||||
|
|
||||||
/// A single logical zone
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct Zone { |
|
||||||
pub id: ZoneId, |
|
||||||
pub name: String, |
|
||||||
/// ID number of the corresponding physical zone
|
|
||||||
pub interface_id: ZoneNum, |
|
||||||
} |
|
||||||
|
|
||||||
pub type ZoneRef = Arc<Zone>; |
|
||||||
|
|
||||||
pub type Zones = im::OrdMap<ZoneId, ZoneRef>; |
|
@ -1,645 +0,0 @@ |
|||||||
//! Scheduling for events to run at certain intervals in the future
|
|
||||||
|
|
||||||
use chrono::{Date, DateTime, Datelike, Local, NaiveDateTime, NaiveTime, TimeZone, Weekday}; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::cmp; |
|
||||||
use std::iter::FromIterator; |
|
||||||
|
|
||||||
pub use chrono::Duration; |
|
||||||
|
|
||||||
/// A set of times of day (for [Schedule](struct.Schedule.html))
|
|
||||||
pub type TimeSet = Vec<NaiveTime>; |
|
||||||
/// A set of days of week (for [Schedule](struct.Schedule.html))
|
|
||||||
pub type WeekdaySet = Vec<Weekday>; |
|
||||||
|
|
||||||
/// Returns a [`WeekdaySet`](type.WeekdaySet.html) of every day of the week
|
|
||||||
#[allow(dead_code)] |
|
||||||
pub fn every_day() -> WeekdaySet { |
|
||||||
WeekdaySet::from_iter( |
|
||||||
[ |
|
||||||
Weekday::Mon, |
|
||||||
Weekday::Tue, |
|
||||||
Weekday::Wed, |
|
||||||
Weekday::Thu, |
|
||||||
Weekday::Fri, |
|
||||||
Weekday::Sat, |
|
||||||
Weekday::Sun, |
|
||||||
] |
|
||||||
.iter() |
|
||||||
.cloned(), |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
/// Represents the different types of date-time bounds that can be on a schedule
|
|
||||||
#[allow(dead_code)] |
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)] |
|
||||||
pub enum DateTimeBound { |
|
||||||
/// There is no bound (ie. the Schedule extends with no limit)
|
|
||||||
None, |
|
||||||
/// There is a bound that repeats every year (ie. the year is set to the current year)
|
|
||||||
Yearly(NaiveDateTime), |
|
||||||
/// There is a definite bound on the schedule
|
|
||||||
Definite(NaiveDateTime), |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for DateTimeBound { |
|
||||||
fn default() -> DateTimeBound { |
|
||||||
DateTimeBound::None |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl DateTimeBound { |
|
||||||
/// Resolves this bound into an optional `DateTime`. If there is no bound or the bound could
|
|
||||||
/// not be resolved, None is returned.
|
|
||||||
///
|
|
||||||
/// `reference` is the reference that is used to resolve a `Yearly` bound.
|
|
||||||
pub fn resolve_from<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> { |
|
||||||
match *self { |
|
||||||
DateTimeBound::None => None, |
|
||||||
DateTimeBound::Yearly(date_time) => { |
|
||||||
date_time.with_year(reference.year()).and_then(|date_time| { |
|
||||||
reference |
|
||||||
.timezone() |
|
||||||
.from_local_datetime(&date_time) |
|
||||||
.single() |
|
||||||
}) |
|
||||||
} |
|
||||||
DateTimeBound::Definite(date_time) => reference |
|
||||||
.timezone() |
|
||||||
.from_local_datetime(&date_time) |
|
||||||
.single(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// A schedule that determines when an event will occur.
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)] |
|
||||||
pub struct Schedule { |
|
||||||
#[serde(
|
|
||||||
serialize_with = "ser::serialize_times", |
|
||||||
deserialize_with = "ser::deserialize_times" |
|
||||||
)] |
|
||||||
pub times: TimeSet, |
|
||||||
#[serde(
|
|
||||||
serialize_with = "ser::serialize_weekdays", |
|
||||||
deserialize_with = "ser::deserialize_weekdays" |
|
||||||
)] |
|
||||||
pub weekdays: WeekdaySet, |
|
||||||
pub from: DateTimeBound, |
|
||||||
pub to: DateTimeBound, |
|
||||||
} |
|
||||||
|
|
||||||
mod ser { |
|
||||||
use super::{DateTimeBound, TimeSet, WeekdaySet}; |
|
||||||
use chrono::{NaiveDate, NaiveTime, Weekday}; |
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
|
||||||
use std::{convert::TryInto, fmt}; |
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)] |
|
||||||
struct TimeOfDay { |
|
||||||
hour: u32, |
|
||||||
minute: u32, |
|
||||||
second: u32, |
|
||||||
#[serde(default)] |
|
||||||
millisecond: u32, |
|
||||||
} |
|
||||||
|
|
||||||
impl From<&NaiveTime> for TimeOfDay { |
|
||||||
fn from(time: &NaiveTime) -> Self { |
|
||||||
use chrono::Timelike; |
|
||||||
Self { |
|
||||||
hour: time.hour(), |
|
||||||
minute: time.minute(), |
|
||||||
second: time.second(), |
|
||||||
millisecond: time.nanosecond() / 1_000_000_u32, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)] |
|
||||||
struct InvalidTimeOfDay; |
|
||||||
|
|
||||||
impl fmt::Display for InvalidTimeOfDay { |
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
||||||
"invalid time of day".fmt(f) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TryInto<NaiveTime> for TimeOfDay { |
|
||||||
type Error = InvalidTimeOfDay; |
|
||||||
fn try_into(self) -> Result<NaiveTime, Self::Error> { |
|
||||||
NaiveTime::from_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond) |
|
||||||
.ok_or(InvalidTimeOfDay) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(clippy::ptr_arg)] |
|
||||||
pub fn serialize_times<S>(times: &TimeSet, serializer: S) -> Result<S::Ok, S::Error> |
|
||||||
where |
|
||||||
S: Serializer, |
|
||||||
{ |
|
||||||
serializer.collect_seq(times.iter().map(TimeOfDay::from)) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn deserialize_times<'de, D>(deserializer: D) -> Result<TimeSet, D::Error> |
|
||||||
where |
|
||||||
D: Deserializer<'de>, |
|
||||||
{ |
|
||||||
use serde::de::{Error, SeqAccess, Visitor}; |
|
||||||
|
|
||||||
struct TimeSetVisitor; |
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for TimeSetVisitor { |
|
||||||
type Value = TimeSet; |
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
|
||||||
formatter.write_str("a sequence of time of days") |
|
||||||
} |
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> |
|
||||||
where |
|
||||||
A: SeqAccess<'de>, |
|
||||||
{ |
|
||||||
let mut times = TimeSet::with_capacity(seq.size_hint().unwrap_or(0)); |
|
||||||
while let Some(value) = seq.next_element::<TimeOfDay>()? { |
|
||||||
let time: NaiveTime = value.try_into().map_err(A::Error::custom)?; |
|
||||||
times.push(time); |
|
||||||
} |
|
||||||
Ok(times) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
deserializer.deserialize_seq(TimeSetVisitor) |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(clippy::ptr_arg)] |
|
||||||
pub fn serialize_weekdays<S>(weekdays: &WeekdaySet, serializer: S) -> Result<S::Ok, S::Error> |
|
||||||
where |
|
||||||
S: Serializer, |
|
||||||
{ |
|
||||||
let iter = weekdays |
|
||||||
.iter() |
|
||||||
.map(|weekday| weekday.num_days_from_sunday()); |
|
||||||
serializer.collect_seq(iter) |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)] |
|
||||||
struct InvalidWeekday; |
|
||||||
|
|
||||||
impl fmt::Display for InvalidWeekday { |
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
||||||
"weekday out of range 0 to 6".fmt(f) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn weekday_from_days_from_sunday(days: u32) -> Result<Weekday, InvalidWeekday> { |
|
||||||
Ok(match days { |
|
||||||
0 => Weekday::Sun, |
|
||||||
1 => Weekday::Mon, |
|
||||||
2 => Weekday::Tue, |
|
||||||
3 => Weekday::Wed, |
|
||||||
4 => Weekday::Thu, |
|
||||||
5 => Weekday::Fri, |
|
||||||
6 => Weekday::Sat, |
|
||||||
_ => return Err(InvalidWeekday), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn deserialize_weekdays<'de, D>(deserializer: D) -> Result<WeekdaySet, D::Error> |
|
||||||
where |
|
||||||
D: Deserializer<'de>, |
|
||||||
{ |
|
||||||
use serde::de::{Error, SeqAccess, Visitor}; |
|
||||||
|
|
||||||
struct WeekdaySetVisitor; |
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for WeekdaySetVisitor { |
|
||||||
type Value = WeekdaySet; |
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
|
||||||
formatter.write_str("a sequence of integers representing weekdays") |
|
||||||
} |
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> |
|
||||||
where |
|
||||||
A: SeqAccess<'de>, |
|
||||||
{ |
|
||||||
let mut weekdays = WeekdaySet::with_capacity(seq.size_hint().unwrap_or(0)); |
|
||||||
while let Some(value) = seq.next_element::<u32>()? { |
|
||||||
let weekday = weekday_from_days_from_sunday(value).map_err(A::Error::custom)?; |
|
||||||
weekdays.push(weekday); |
|
||||||
} |
|
||||||
Ok(weekdays) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
deserializer.deserialize_seq(WeekdaySetVisitor) |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
||||||
struct DateAndYear { |
|
||||||
year: i32, |
|
||||||
month: u32, |
|
||||||
day: u32, |
|
||||||
} |
|
||||||
|
|
||||||
impl From<NaiveDate> for DateAndYear { |
|
||||||
fn from(date: NaiveDate) -> Self { |
|
||||||
use chrono::Datelike; |
|
||||||
Self { |
|
||||||
year: date.year(), |
|
||||||
month: date.month(), |
|
||||||
day: date.day(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)] |
|
||||||
struct InvalidDateAndYear; |
|
||||||
|
|
||||||
impl fmt::Display for InvalidDateAndYear { |
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
||||||
"invalid date or year".fmt(f) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TryInto<NaiveDate> for DateAndYear { |
|
||||||
type Error = InvalidDateAndYear; |
|
||||||
fn try_into(self) -> Result<NaiveDate, Self::Error> { |
|
||||||
NaiveDate::from_ymd_opt(self.year, self.month, self.day).ok_or(InvalidDateAndYear) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Serialize for DateTimeBound { |
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
|
||||||
where |
|
||||||
S: Serializer, |
|
||||||
{ |
|
||||||
match self { |
|
||||||
DateTimeBound::None => serializer.serialize_none(), |
|
||||||
DateTimeBound::Yearly(date_time) => { |
|
||||||
// Discard time
|
|
||||||
let mut date_of_year: DateAndYear = date_time.date().into(); |
|
||||||
// Set year to 0 (since it is yearly)
|
|
||||||
date_of_year.year = 0; |
|
||||||
|
|
||||||
date_of_year.serialize(serializer) |
|
||||||
} |
|
||||||
DateTimeBound::Definite(date_time) => { |
|
||||||
// Discard time
|
|
||||||
let date_of_year: DateAndYear = date_time.date().into(); |
|
||||||
|
|
||||||
date_of_year.serialize(serializer) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for DateTimeBound { |
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
|
||||||
where |
|
||||||
D: Deserializer<'de>, |
|
||||||
{ |
|
||||||
use serde::de::Error; |
|
||||||
let date_of_year: Option<DateAndYear> = Deserialize::deserialize(deserializer)?; |
|
||||||
Ok(match date_of_year { |
|
||||||
Some(date_of_year) => { |
|
||||||
let year = date_of_year.year; |
|
||||||
let date: NaiveDate = date_of_year.try_into().map_err(D::Error::custom)?; |
|
||||||
let date_time = date.and_hms(0, 0, 0); |
|
||||||
if year == 0 { |
|
||||||
DateTimeBound::Yearly(date_time) |
|
||||||
} else { |
|
||||||
DateTimeBound::Definite(date_time) |
|
||||||
} |
|
||||||
} |
|
||||||
None => DateTimeBound::None, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Gets the next date matching the `weekday` after `date`
|
|
||||||
fn next_weekday<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> { |
|
||||||
while date.weekday() != weekday { |
|
||||||
date = date.succ(); |
|
||||||
} |
|
||||||
date |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(dead_code)] |
|
||||||
impl Schedule { |
|
||||||
/// Creates a new Schedule.
|
|
||||||
///
|
|
||||||
/// `times` is the times of day the event will be run. `weekdays` is the set of days of week
|
|
||||||
/// the event will be run. `from` and `to` are restrictions on the end and beginning of event
|
|
||||||
/// runs, respectively.
|
|
||||||
pub fn new<T, W>(times: T, weekdays: W, from: DateTimeBound, to: DateTimeBound) -> Schedule |
|
||||||
where |
|
||||||
T: IntoIterator<Item = NaiveTime>, |
|
||||||
W: IntoIterator<Item = Weekday>, |
|
||||||
{ |
|
||||||
Schedule { |
|
||||||
times: TimeSet::from_iter(times), |
|
||||||
weekdays: WeekdaySet::from_iter(weekdays), |
|
||||||
from, |
|
||||||
to, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Gets the next `DateTime` the event should run after `reference`
|
|
||||||
///
|
|
||||||
/// Returns `None` if the event will never run after `reference` (ie. must be a `from` bound)
|
|
||||||
pub fn next_run_after<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> { |
|
||||||
let mut to = self.to.resolve_from(reference); |
|
||||||
let mut from = self.from.resolve_from(reference); |
|
||||||
if let (Some(from), Some(to)) = (&mut from, &mut to) { |
|
||||||
// We must handle the case where yearly bounds cross a year boundary
|
|
||||||
if to < from { |
|
||||||
if reference < to { |
|
||||||
// Still in the bounds overlapping the previous year boundary
|
|
||||||
*from = from.with_year(from.year() - 1).unwrap(); |
|
||||||
} else { |
|
||||||
// Awaiting (or in) next years bounds
|
|
||||||
*to = to.with_year(to.year() + 1).unwrap(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
let from = match (from, &to) { |
|
||||||
(Some(from), Some(to)) if &from > to => from.with_year(from.year() + 1), |
|
||||||
(from, _) => from, |
|
||||||
}; |
|
||||||
let reference = match &from { |
|
||||||
Some(from) if from > reference => from, |
|
||||||
_ => reference, |
|
||||||
} |
|
||||||
.clone(); |
|
||||||
let mut next_run: Option<DateTime<Tz>> = None; |
|
||||||
for weekday in &self.weekdays { |
|
||||||
for time in &self.times { |
|
||||||
let candidate = next_weekday(reference.date(), *weekday) |
|
||||||
.and_time(*time) |
|
||||||
.map(|date| { |
|
||||||
if date < reference { |
|
||||||
date + Duration::weeks(1) |
|
||||||
} else { |
|
||||||
date |
|
||||||
} |
|
||||||
}); |
|
||||||
let candidate = match (candidate, &to) { |
|
||||||
(Some(date), Some(to)) if &date > to => None, |
|
||||||
(date, _) => date, |
|
||||||
}; |
|
||||||
next_run = match (next_run, candidate) { |
|
||||||
// return whichever is first if there are 2 candidates
|
|
||||||
(Some(d1), Some(d2)) => Some(cmp::min(d1, d2)), |
|
||||||
// otherwise return whichever isn't None (or None if both are)
|
|
||||||
(o1, o2) => o1.or(o2), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
next_run |
|
||||||
} |
|
||||||
|
|
||||||
/// Gets the next run after the current (local) time
|
|
||||||
///
|
|
||||||
/// See [next_run_after](#method.next_run_after)
|
|
||||||
pub fn next_run_local(&self) -> Option<DateTime<Local>> { |
|
||||||
self.next_run_after(&Local::now()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod test { |
|
||||||
use super::*; |
|
||||||
use chrono::NaiveDate; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_date_time_bound() { |
|
||||||
use super::DateTimeBound::*; |
|
||||||
|
|
||||||
let cases: Vec<(DateTimeBound, Option<DateTime<Local>>)> = vec![ |
|
||||||
(None, Option::None), |
|
||||||
( |
|
||||||
Definite(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)), |
|
||||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
|
||||||
), |
|
||||||
( |
|
||||||
Yearly(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)), |
|
||||||
Some(Local.ymd(2018, 11, 16).and_hms(10, 30, 0)), |
|
||||||
), |
|
||||||
( |
|
||||||
Yearly(NaiveDate::from_ymd(2016, 1, 1).and_hms(0, 0, 0)), |
|
||||||
Some(Local.ymd(2018, 1, 1).and_hms(0, 0, 0)), |
|
||||||
), |
|
||||||
( |
|
||||||
Yearly(NaiveDate::from_ymd(2016, 12, 31).and_hms(23, 59, 59)), |
|
||||||
Some(Local.ymd(2018, 12, 31).and_hms(23, 59, 59)), |
|
||||||
), |
|
||||||
( |
|
||||||
Yearly(NaiveDate::from_ymd(2012, 2, 29).and_hms(0, 0, 0)), |
|
||||||
Option::None, |
|
||||||
), /* leap day */ |
|
||||||
]; |
|
||||||
let from = Local.ymd(2018, 1, 1).and_hms(0, 0, 0); |
|
||||||
|
|
||||||
for (bound, expected_result) in cases { |
|
||||||
let result = bound.resolve_from(&from); |
|
||||||
assert_eq!(result, expected_result); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_next_weekday() { |
|
||||||
use super::next_weekday; |
|
||||||
use chrono::Weekday; |
|
||||||
// (date, weekday, result)
|
|
||||||
let cases: Vec<(Date<Local>, Weekday, Date<Local>)> = vec![ |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16), |
|
||||||
Weekday::Wed, |
|
||||||
Local.ymd(2016, 11, 16), |
|
||||||
), |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16), |
|
||||||
Weekday::Fri, |
|
||||||
Local.ymd(2016, 11, 18), |
|
||||||
), |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16), |
|
||||||
Weekday::Tue, |
|
||||||
Local.ymd(2016, 11, 22), |
|
||||||
), |
|
||||||
(Local.ymd(2016, 12, 30), Weekday::Tue, Local.ymd(2017, 1, 3)), |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16), |
|
||||||
Weekday::Tue, |
|
||||||
Local.ymd(2016, 11, 22), |
|
||||||
), |
|
||||||
]; |
|
||||||
|
|
||||||
for (date, weekday, expected_result) in cases { |
|
||||||
let result = next_weekday(date, weekday); |
|
||||||
assert_eq!(result, expected_result); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_next_run_after() { |
|
||||||
use super::{DateTimeBound, Schedule}; |
|
||||||
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday}; |
|
||||||
let schedule = Schedule::new( |
|
||||||
vec![NaiveTime::from_hms(10, 30, 0)], |
|
||||||
vec![Weekday::Wed], |
|
||||||
DateTimeBound::None, |
|
||||||
DateTimeBound::None, |
|
||||||
); |
|
||||||
let cases: Vec<(DateTime<Local>, Option<DateTime<Local>>)> = vec![ |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 14).and_hms(10, 30, 0), |
|
||||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
|
||||||
), |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16).and_hms(10, 20, 0), |
|
||||||
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)), |
|
||||||
), |
|
||||||
( |
|
||||||
Local.ymd(2016, 11, 16).and_hms(10, 40, 0), |
|
||||||
Some(Local.ymd(2016, 11, 23).and_hms(10, 30, 0)), |
|
||||||
), |
|
||||||
]; |
|
||||||
for (reference, expected_result) in cases { |
|
||||||
let result = schedule.next_run_after(&reference); |
|
||||||
assert_eq!(result, expected_result); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_next_run_after2() { |
|
||||||
use super::{DateTimeBound, Schedule}; |
|
||||||
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday}; |
|
||||||
#[derive(Debug)] |
|
||||||
struct Case { |
|
||||||
schedule: Schedule, |
|
||||||
ref_time: DateTime<Local>, |
|
||||||
expected_result: Option<DateTime<Local>>, |
|
||||||
} |
|
||||||
impl Case { |
|
||||||
fn new( |
|
||||||
schedule: Schedule, |
|
||||||
ref_time: DateTime<Local>, |
|
||||||
expected_result: Option<DateTime<Local>>, |
|
||||||
) -> Self { |
|
||||||
Self { |
|
||||||
schedule, |
|
||||||
ref_time, |
|
||||||
expected_result, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
let sched1 = Schedule::new( |
|
||||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
|
||||||
vec![Weekday::Thu, Weekday::Fri], |
|
||||||
DateTimeBound::None, |
|
||||||
DateTimeBound::None, |
|
||||||
); |
|
||||||
let sched2 = Schedule::new( |
|
||||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
|
||||||
vec![Weekday::Thu, Weekday::Fri], |
|
||||||
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 5, 30).and_hms(0, 0, 0)), |
|
||||||
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 6, 30).and_hms(0, 0, 0)), |
|
||||||
); |
|
||||||
let sched3 = Schedule::new( |
|
||||||
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)], |
|
||||||
every_day(), |
|
||||||
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 12, 15).and_hms(0, 0, 0)), |
|
||||||
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 15).and_hms(0, 0, 0)), |
|
||||||
); |
|
||||||
let cases: Vec<Case> = vec![ |
|
||||||
Case::new( |
|
||||||
sched1.clone(), |
|
||||||
Local.ymd(2016, 5, 16).and_hms(0, 0, 0), |
|
||||||
Some(Local.ymd(2016, 5, 19).and_hms(8, 30, 0)), |
|
||||||
), |
|
||||||
Case::new( |
|
||||||
sched1, |
|
||||||
Local.ymd(2016, 5, 20).and_hms(9, 0, 0), |
|
||||||
Some(Local.ymd(2016, 5, 20).and_hms(20, 0, 0)), |
|
||||||
), |
|
||||||
Case::new( |
|
||||||
sched2.clone(), |
|
||||||
Local.ymd(2016, 6, 1).and_hms(0, 0, 0), |
|
||||||
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)), |
|
||||||
), |
|
||||||
Case::new( |
|
||||||
sched2.clone(), |
|
||||||
Local.ymd(2016, 5, 1).and_hms(0, 0, 0), |
|
||||||
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)), |
|
||||||
), |
|
||||||
Case::new(sched2, Local.ymd(2016, 7, 1).and_hms(0, 0, 0), None), |
|
||||||
Case::new( |
|
||||||
sched3.clone(), |
|
||||||
Local.ymd(2016, 11, 1).and_hms(0, 0, 0), |
|
||||||
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)), |
|
||||||
), |
|
||||||
Case::new( |
|
||||||
sched3.clone(), |
|
||||||
Local.ymd(2017, 1, 1).and_hms(9, 0, 0), |
|
||||||
Some(Local.ymd(2017, 1, 1).and_hms(20, 0, 0)), |
|
||||||
), |
|
||||||
Case::new( |
|
||||||
sched3, |
|
||||||
Local.ymd(2016, 1, 30).and_hms(0, 0, 0), |
|
||||||
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)), |
|
||||||
), |
|
||||||
]; |
|
||||||
for case in cases { |
|
||||||
let result = case.schedule.next_run_after(&case.ref_time); |
|
||||||
assert_eq!(result, case.expected_result, "case failed: {:?}", case); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_serialize() { |
|
||||||
let sched = Schedule::new( |
|
||||||
vec![ |
|
||||||
NaiveTime::from_hms_milli(0, 0, 0, 0), |
|
||||||
NaiveTime::from_hms_milli(23, 59, 59, 999), |
|
||||||
], |
|
||||||
every_day(), |
|
||||||
DateTimeBound::Yearly(NaiveDate::from_ymd(2020, 1, 1).and_hms(0, 0, 0)), |
|
||||||
DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(23, 59, 59)), |
|
||||||
); |
|
||||||
let ser = serde_json::to_string(&sched).unwrap(); |
|
||||||
// Weekdays should match the order in `every_day()` but with sunday being 0
|
|
||||||
assert_eq!( |
|
||||||
&ser, |
|
||||||
"{\ |
|
||||||
\"times\":[\ |
|
||||||
{\"hour\":0,\"minute\":0,\"second\":0,\"millisecond\":0},\ |
|
||||||
{\"hour\":23,\"minute\":59,\"second\":59,\"millisecond\":999}\ |
|
||||||
],\ |
|
||||||
\"weekdays\":[1,2,3,4,5,6,0],\ |
|
||||||
\"from\":{\ |
|
||||||
\"year\":0,\"month\":1,\"day\":1\ |
|
||||||
},\ |
|
||||||
\"to\":{\ |
|
||||||
\"year\":9999,\"month\":12,\"day\":31\ |
|
||||||
}\ |
|
||||||
}" |
|
||||||
); |
|
||||||
let sched_de: Schedule = serde_json::from_str(&ser).unwrap(); |
|
||||||
assert_eq!(sched.times, sched_de.times); |
|
||||||
assert_eq!(sched.weekdays, sched_de.weekdays); |
|
||||||
// This serialization is lossy (year is discarded for yearly)
|
|
||||||
// assert_eq!(sched_de.from, sched.from);
|
|
||||||
assert_eq!( |
|
||||||
sched_de.from, |
|
||||||
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)) |
|
||||||
); |
|
||||||
// This serialization is also lossy (time is discarded)
|
|
||||||
// assert_eq!(sched_de.to, sched.to);
|
|
||||||
assert_eq!( |
|
||||||
sched_de.to, |
|
||||||
DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(0, 0, 0)) |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
|
||||||
|
|
||||||
pub mod duration_secs { |
|
||||||
use super::*; |
|
||||||
use std::time::Duration; |
|
||||||
|
|
||||||
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> |
|
||||||
where |
|
||||||
S: Serializer, |
|
||||||
{ |
|
||||||
duration.as_secs_f64().serialize(serializer) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error> |
|
||||||
where |
|
||||||
D: Deserializer<'de>, |
|
||||||
{ |
|
||||||
let secs: f64 = Deserialize::deserialize(deserializer)?; |
|
||||||
Ok(Duration::from_secs_f64(secs)) |
|
||||||
} |
|
||||||
} |
|
@ -1,65 +0,0 @@ |
|||||||
use std::iter::repeat_with; |
|
||||||
use std::sync::atomic::{AtomicBool, Ordering}; |
|
||||||
use tracing::debug; |
|
||||||
|
|
||||||
pub type ZoneNum = u32; |
|
||||||
|
|
||||||
pub trait ZoneInterface: Send + Sync { |
|
||||||
fn num_zones(&self) -> ZoneNum; |
|
||||||
fn set_zone_state(&self, id: ZoneNum, running: bool); |
|
||||||
fn get_zone_state(&self, id: ZoneNum) -> bool; |
|
||||||
} |
|
||||||
|
|
||||||
pub struct MockZoneInterface { |
|
||||||
states: Vec<AtomicBool>, |
|
||||||
} |
|
||||||
|
|
||||||
impl MockZoneInterface { |
|
||||||
#[allow(dead_code)] |
|
||||||
pub fn new(num_zones: ZoneNum) -> Self { |
|
||||||
Self { |
|
||||||
states: repeat_with(|| AtomicBool::new(false)) |
|
||||||
.take(num_zones as usize) |
|
||||||
.collect(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneInterface for MockZoneInterface { |
|
||||||
fn num_zones(&self) -> ZoneNum { |
|
||||||
self.states.len() as ZoneNum |
|
||||||
} |
|
||||||
fn set_zone_state(&self, id: ZoneNum, running: bool) { |
|
||||||
debug!(id, running, "setting zone"); |
|
||||||
self.states[id as usize].store(running, Ordering::SeqCst); |
|
||||||
} |
|
||||||
fn get_zone_state(&self, id: ZoneNum) -> bool { |
|
||||||
self.states[id as usize].load(Ordering::SeqCst) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod test { |
|
||||||
use super::*; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_mock_zone_interface() { |
|
||||||
let iface = MockZoneInterface::new(6); |
|
||||||
assert_eq!(iface.num_zones(), 6); |
|
||||||
for i in 0..6u32 { |
|
||||||
assert_eq!(iface.get_zone_state(i), false); |
|
||||||
} |
|
||||||
for i in 0..6u32 { |
|
||||||
iface.set_zone_state(i, true); |
|
||||||
} |
|
||||||
for i in 0..6u32 { |
|
||||||
assert_eq!(iface.get_zone_state(i), true); |
|
||||||
} |
|
||||||
for i in 0..6u32 { |
|
||||||
iface.set_zone_state(i, false); |
|
||||||
} |
|
||||||
for i in 0..6u32 { |
|
||||||
assert_eq!(iface.get_zone_state(i), false); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_database" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[features] |
|
||||||
bundled = ["rusqlite/bundled"] |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
sprinklers_core = { path = "../sprinklers_core" } |
|
||||||
rusqlite = "0.24.0" |
|
||||||
eyre = "0.6.0" |
|
||||||
serde = { version = "1.0.116" } |
|
||||||
serde_json = "1.0.57" |
|
||||||
thiserror = "1.0.20" |
|
||||||
tracing = { version = "0.1.19" } |
|
@ -1,65 +0,0 @@ |
|||||||
#!/usr/bin/env bash |
|
||||||
|
|
||||||
shopt -s nullglob extglob |
|
||||||
set -e |
|
||||||
|
|
||||||
usage() { |
|
||||||
echo "Usage: $0 <migration_name>" |
|
||||||
} |
|
||||||
|
|
||||||
SCRIPT=$(realpath "$0") |
|
||||||
SCRIPTPATH=$(dirname "$SCRIPT") |
|
||||||
|
|
||||||
PROJECT_ROOT=$(realpath --relative-to="$PWD" "$SCRIPTPATH/..") |
|
||||||
|
|
||||||
MIGRATION_NAME="$1" |
|
||||||
if [[ -z "$MIGRATION_NAME" ]]; then |
|
||||||
usage |
|
||||||
exit 1 |
|
||||||
fi |
|
||||||
|
|
||||||
MIGRATIONS_DIR="$PROJECT_ROOT/sprinklers_database/src/migrations" |
|
||||||
|
|
||||||
# echo "MIGRATIONS_DIR: $MIGRATIONS_DIR" |
|
||||||
|
|
||||||
pushd "$MIGRATIONS_DIR" >/dev/null |
|
||||||
SQL_FILES=(+([0-9])*.sql) |
|
||||||
popd >/dev/null |
|
||||||
|
|
||||||
# echo "SQL_FILES: " |
|
||||||
# echo "${SQL_FILES[@]}" |
|
||||||
|
|
||||||
# Remove anything after first numbers |
|
||||||
MIGRATION_NUMBERS=("${SQL_FILES[@]%%-*.sql}") |
|
||||||
|
|
||||||
# echo "MIGRATION_NUMBERS: " |
|
||||||
# echo "${MIGRATION_NUMBERS[@]}" |
|
||||||
|
|
||||||
MIGRATION_MAX=0 |
|
||||||
for num in "${MIGRATION_NUMBERS[@]}"; do |
|
||||||
if (( num > MIGRATION_MAX )) |
|
||||||
then |
|
||||||
MIGRATION_MAX=$num |
|
||||||
fi |
|
||||||
done |
|
||||||
|
|
||||||
# echo "MIGRATION_MAX: $MIGRATION_MAX" |
|
||||||
|
|
||||||
NEXT_MIGRATION=$(( MIGRATION_MAX + 1 )) |
|
||||||
NEXT_MIGRATION_PREFIX=$( printf "%04d" "$NEXT_MIGRATION" ) |
|
||||||
|
|
||||||
# echo "NEXT_MIGRATION: $NEXT_MIGRATION" |
|
||||||
# echo "NEXT_MIGRATION_PREFIX: $NEXT_MIGRATION_PREFIX" |
|
||||||
|
|
||||||
for SUFFIX in "up" "down"; do |
|
||||||
migration_file="$MIGRATIONS_DIR/$NEXT_MIGRATION_PREFIX-$MIGRATION_NAME-$SUFFIX.sql" |
|
||||||
echo "Creating migration file $migration_file" |
|
||||||
touch "$migration_file" |
|
||||||
done |
|
||||||
|
|
||||||
LINE_TO_INSERT="\ \ \ \ migs.add(include_file_migration!($NEXT_MIGRATION, \"$NEXT_MIGRATION_PREFIX-$MIGRATION_NAME\"));" |
|
||||||
|
|
||||||
MIGRATIONS_RS="$PROJECT_ROOT/sprinklers_database/src/migrations/mod.rs" |
|
||||||
echo "Inserting line in $MIGRATIONS_RS" |
|
||||||
sed -i "/INSERT MIGRATION ABOVE/i \ |
|
||||||
$LINE_TO_INSERT" "$MIGRATIONS_RS" |
|
@ -1,43 +0,0 @@ |
|||||||
mod migration; |
|
||||||
mod migrations; |
|
||||||
mod program; |
|
||||||
mod sql_json; |
|
||||||
mod zone; |
|
||||||
|
|
||||||
pub use migration::*; |
|
||||||
pub use migrations::create_migrations; |
|
||||||
pub use program::*; |
|
||||||
|
|
||||||
pub use rusqlite::Connection as DbConn; |
|
||||||
|
|
||||||
use sprinklers_core::model::Zones; |
|
||||||
|
|
||||||
use eyre::Result; |
|
||||||
use rusqlite::NO_PARAMS; |
|
||||||
|
|
||||||
pub fn setup_db() -> Result<DbConn> { |
|
||||||
// let conn = DbConn::open_in_memory()?;
|
|
||||||
let mut conn = DbConn::open("test.db")?; |
|
||||||
|
|
||||||
// Go ahead and use write ahead log for better perf
|
|
||||||
conn.execute_batch("PRAGMA journal_mode=WAL;")?; |
|
||||||
|
|
||||||
let migs = create_migrations(); |
|
||||||
migs.apply(&mut conn)?; |
|
||||||
|
|
||||||
Ok(conn) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn query_zones(conn: &DbConn) -> Result<Zones> { |
|
||||||
let mut statement = conn.prepare_cached( |
|
||||||
"SELECT s.id, s.name, s.interface_id \ |
|
||||||
FROM sections AS s;", |
|
||||||
)?; |
|
||||||
let rows = statement.query_map(NO_PARAMS, zone::from_sql)?; |
|
||||||
let mut zones = Zones::new(); |
|
||||||
for row in rows { |
|
||||||
let zone = row?; |
|
||||||
zones.insert(zone.id, zone.into()); |
|
||||||
} |
|
||||||
Ok(zones) |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
DROP TABLE sections; |
|
@ -1,5 +0,0 @@ |
|||||||
CREATE TABLE sections ( |
|
||||||
id INTEGER PRIMARY KEY, |
|
||||||
name TEXT NOT NULL, |
|
||||||
interface_id INTEGER NOT NULL |
|
||||||
); |
|
@ -1 +0,0 @@ |
|||||||
DELETE FROM sections; |
|
@ -1,7 +0,0 @@ |
|||||||
INSERT INTO sections (id, name, interface_id) |
|
||||||
VALUES (1, 'Front Yard Middle', 0), |
|
||||||
(2, 'Front Yard Left', 1), |
|
||||||
(3, 'Front Yard Right', 2), |
|
||||||
(4, 'Back Yard Middle', 3), |
|
||||||
(5, 'Back Yard Sauna', 4), |
|
||||||
(6, 'Garden', 5); |
|
@ -1,3 +0,0 @@ |
|||||||
DROP INDEX program_sequence_items_idx1; |
|
||||||
DROP TABLE program_sequence_items; |
|
||||||
DROP TABLE programs; |
|
@ -1,20 +0,0 @@ |
|||||||
CREATE TABLE programs |
|
||||||
( |
|
||||||
id INTEGER PRIMARY KEY, |
|
||||||
name TEXT NOT NULL, |
|
||||||
enabled BOOLEAN NOT NULL, |
|
||||||
schedule TEXT NOT NULL |
|
||||||
); |
|
||||||
|
|
||||||
CREATE TABLE program_sequence_items |
|
||||||
( |
|
||||||
seq_num INTEGER NOT NULL, |
|
||||||
program_id INTEGER NOT NULL, |
|
||||||
section_id INTEGER NOT NULL, |
|
||||||
duration REAL NOT NULL, |
|
||||||
FOREIGN KEY (program_id) REFERENCES programs (id), |
|
||||||
FOREIGN KEY (section_id) REFERENCES sections (id) |
|
||||||
); |
|
||||||
|
|
||||||
CREATE UNIQUE INDEX program_sequence_items_idx1 |
|
||||||
ON program_sequence_items (program_id, seq_num); |
|
@ -1,2 +0,0 @@ |
|||||||
DELETE FROM program_sequence_items; |
|
||||||
DELETE FROM programs; |
|
@ -1,14 +0,0 @@ |
|||||||
INSERT INTO programs (id, name, enabled, schedule) |
|
||||||
VALUES (1, 'Test Program', TRUE, |
|
||||||
json_object( |
|
||||||
'times', json('[{"hour": 16, "minute": 1, "second": 0}]'), |
|
||||||
'weekdays', json_array(0, 1, 2, 3, 4, 5, 6), |
|
||||||
'from', NULL, |
|
||||||
'to', NULL)); |
|
||||||
|
|
||||||
INSERT INTO program_sequence_items (seq_num, program_id, section_id, duration) |
|
||||||
SELECT row_number() OVER (ORDER BY s.id) seq_num, |
|
||||||
(SELECT p.id FROM programs as p WHERE p.name = 'Test Program') program_id, |
|
||||||
s.id section_id, |
|
||||||
2.0 duration |
|
||||||
FROM sections AS s; |
|
@ -1 +0,0 @@ |
|||||||
DROP VIEW program_sequences; |
|
@ -1,6 +0,0 @@ |
|||||||
CREATE VIEW program_sequences AS |
|
||||||
SELECT psi.program_id program_id, |
|
||||||
json_group_array(json_object( |
|
||||||
'section_id', psi.section_id, |
|
||||||
'duration', psi.duration)) sequence |
|
||||||
FROM program_sequence_items as psi; |
|
@ -1,8 +0,0 @@ |
|||||||
DROP VIEW program_sequences; |
|
||||||
|
|
||||||
CREATE VIEW program_sequences AS |
|
||||||
SELECT psi.program_id program_id, |
|
||||||
json_group_array(json_object( |
|
||||||
'section_id', psi.section_id, |
|
||||||
'duration', psi.duration)) sequence |
|
||||||
FROM program_sequence_items as psi; |
|
@ -1,15 +0,0 @@ |
|||||||
DROP VIEW program_sequences; |
|
||||||
|
|
||||||
CREATE VIEW program_sequences AS |
|
||||||
WITH psi_sorted AS ( |
|
||||||
SELECT psi.program_id program_id, |
|
||||||
json_object( |
|
||||||
'sectionId', psi.section_id, |
|
||||||
'duration', psi.duration) |
|
||||||
obj |
|
||||||
FROM program_sequence_items AS psi |
|
||||||
ORDER BY psi.program_id, psi.seq_num) |
|
||||||
SELECT psi_sorted.program_id program_id, |
|
||||||
json_group_array(json(psi_sorted.obj)) sequence |
|
||||||
FROM psi_sorted |
|
||||||
GROUP BY psi_sorted.program_id; |
|
@ -1,23 +0,0 @@ |
|||||||
use super::migration::{Migrations, SimpleMigration}; |
|
||||||
|
|
||||||
macro_rules! include_file_migration { |
|
||||||
($mig_version:expr, $mig_name:literal) => { |
|
||||||
SimpleMigration::new_box( |
|
||||||
$mig_version, |
|
||||||
include_str!(concat!($mig_name, "-up.sql")), |
|
||||||
include_str!(concat!($mig_name, "-down.sql")), |
|
||||||
) |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
pub fn create_migrations() -> Migrations { |
|
||||||
let mut migs = Migrations::new(); |
|
||||||
migs.add(include_file_migration!(1, "0001-table_sections")); |
|
||||||
migs.add(include_file_migration!(2, "0002-section_rows")); |
|
||||||
migs.add(include_file_migration!(3, "0003-table_programs")); |
|
||||||
migs.add(include_file_migration!(4, "0004-program_rows")); |
|
||||||
migs.add(include_file_migration!(5, "0005-view_program_sequence")); |
|
||||||
migs.add(include_file_migration!(6, "0006-fix_view_program_seq")); |
|
||||||
// INSERT MIGRATION ABOVE -- DO NOT EDIT THIS COMMENT
|
|
||||||
migs |
|
||||||
} |
|
@ -1,225 +0,0 @@ |
|||||||
use super::sql_json::SqlJson; |
|
||||||
use super::DbConn; |
|
||||||
use sprinklers_core::{ |
|
||||||
model::{ |
|
||||||
Program, ProgramId, ProgramItem, ProgramSequence, ProgramUpdateData, Programs, ZoneId, |
|
||||||
}, |
|
||||||
schedule::Schedule, |
|
||||||
}; |
|
||||||
|
|
||||||
use eyre::Result; |
|
||||||
use rusqlite::{params, Row, ToSql, Transaction, NO_PARAMS}; |
|
||||||
use thiserror::Error; |
|
||||||
|
|
||||||
type SqlProgramSequence = SqlJson<ProgramSequence>; |
|
||||||
type SqlSchedule = SqlJson<Schedule>; |
|
||||||
|
|
||||||
fn from_sql<'a>(row: &Row<'a>) -> rusqlite::Result<Program> { |
|
||||||
Ok(Program { |
|
||||||
id: row.get(0)?, |
|
||||||
name: row.get(1)?, |
|
||||||
enabled: row.get(2)?, |
|
||||||
schedule: row.get::<_, SqlSchedule>(3)?.into_inner(), |
|
||||||
sequence: row.get::<_, SqlProgramSequence>(4)?.into_inner(), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
struct SqlProgramUpdate<'a> { |
|
||||||
id: ProgramId, |
|
||||||
name: Option<&'a String>, |
|
||||||
enabled: Option<bool>, |
|
||||||
schedule: Option<SqlJson<&'a Schedule>>, |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a SqlProgramUpdate<'a> { |
|
||||||
type Item = &'a dyn ToSql; |
|
||||||
type IntoIter = std::vec::IntoIter<Self::Item>; |
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter { |
|
||||||
vec![ |
|
||||||
&self.id as &dyn ToSql, |
|
||||||
&self.name, |
|
||||||
&self.enabled, |
|
||||||
&self.schedule, |
|
||||||
] |
|
||||||
.into_iter() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn update_as_sql(id: ProgramId, program: &ProgramUpdateData) -> SqlProgramUpdate { |
|
||||||
SqlProgramUpdate { |
|
||||||
id, |
|
||||||
name: program.name.as_ref(), |
|
||||||
enabled: program.enabled, |
|
||||||
schedule: program.schedule.as_ref().map(SqlJson), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
struct SqlProgramItem { |
|
||||||
program_id: ProgramId, |
|
||||||
seq_num: isize, |
|
||||||
zone_id: ZoneId, |
|
||||||
duration: f64, |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a SqlProgramItem { |
|
||||||
type Item = &'a dyn ToSql; |
|
||||||
type IntoIter = std::vec::IntoIter<Self::Item>; |
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter { |
|
||||||
vec![ |
|
||||||
&self.program_id as &dyn ToSql, |
|
||||||
&self.seq_num, |
|
||||||
&self.zone_id, |
|
||||||
&self.duration, |
|
||||||
] |
|
||||||
.into_iter() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn item_as_sql( |
|
||||||
program_item: &ProgramItem, |
|
||||||
program_id: ProgramId, |
|
||||||
seq_num: usize, |
|
||||||
) -> SqlProgramItem { |
|
||||||
SqlProgramItem { |
|
||||||
program_id, |
|
||||||
seq_num: (seq_num + 1) as isize, |
|
||||||
zone_id: program_item.zone_id, |
|
||||||
duration: program_item.duration.as_secs_f64(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn sequence_as_sql<'a>( |
|
||||||
program_id: ProgramId, |
|
||||||
sequence: &'a [ProgramItem], |
|
||||||
) -> impl Iterator<Item = SqlProgramItem> + 'a { |
|
||||||
sequence |
|
||||||
.iter() |
|
||||||
.enumerate() |
|
||||||
.map(move |(seq_num, item)| item_as_sql(item, program_id, seq_num)) |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Error)] |
|
||||||
#[error("no such program id: {0}")] |
|
||||||
pub struct NoSuchProgram(pub ProgramId); |
|
||||||
|
|
||||||
pub fn query_programs(conn: &DbConn) -> Result<Programs> { |
|
||||||
let query_sql = "\ |
|
||||||
SELECT p.id, p.name, p.enabled, p.schedule, ps.sequence |
|
||||||
FROM programs AS p |
|
||||||
INNER JOIN program_sequences AS ps ON ps.program_id = p.id;"; |
|
||||||
let mut statement = conn.prepare_cached(query_sql)?; |
|
||||||
let rows = statement.query_map(NO_PARAMS, from_sql)?; |
|
||||||
let mut programs = Programs::new(); |
|
||||||
for row in rows { |
|
||||||
let program = row?; |
|
||||||
programs.insert(program.id, program.into()); |
|
||||||
} |
|
||||||
Ok(programs) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn query_program_by_id(conn: &DbConn, id: ProgramId) -> Result<Program> { |
|
||||||
let query_sql = "\ |
|
||||||
SELECT p.id, p.name, p.enabled, p.schedule, ps.sequence |
|
||||||
FROM programs AS p |
|
||||||
INNER JOIN program_sequences AS ps ON ps.program_id = p.id |
|
||||||
WHERE p.id = ?1;"; |
|
||||||
let mut statement = conn.prepare_cached(query_sql)?; |
|
||||||
statement |
|
||||||
.query_row(params![id], from_sql) |
|
||||||
.map_err(|err| match err { |
|
||||||
rusqlite::Error::QueryReturnedNoRows => NoSuchProgram(id).into(), |
|
||||||
e => e.into(), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn update_program( |
|
||||||
trans: &mut Transaction, |
|
||||||
id: ProgramId, |
|
||||||
prog: &ProgramUpdateData, |
|
||||||
) -> Result<()> { |
|
||||||
let save = trans.savepoint()?; |
|
||||||
let conn = &*save; |
|
||||||
let update_sql = "\ |
|
||||||
UPDATE programs |
|
||||||
SET name = ifnull(?2, name), |
|
||||||
enabled = ifnull(?3, enabled), |
|
||||||
schedule = ifnull(?4, schedule) |
|
||||||
WHERE id = ?1;"; |
|
||||||
let updated = conn |
|
||||||
.prepare_cached(update_sql)? |
|
||||||
.execute(&update_as_sql(id, prog))?; |
|
||||||
if updated == 0 { |
|
||||||
return Err(NoSuchProgram(id).into()); |
|
||||||
} |
|
||||||
if let Some(sequence) = &prog.sequence { |
|
||||||
let clear_seq_sql = "\ |
|
||||||
DELETE |
|
||||||
FROM program_sequence_items |
|
||||||
WHERE program_id = ?1;"; |
|
||||||
conn.prepare_cached(clear_seq_sql)?.execute(params![id])?; |
|
||||||
let insert_seq_sql = "\ |
|
||||||
INSERT INTO program_sequence_items (program_id, seq_num, section_id, duration) |
|
||||||
VALUES (?1, ?2, ?3, ?4);"; |
|
||||||
let mut insert_seq = conn.prepare_cached(insert_seq_sql)?; |
|
||||||
for params in sequence_as_sql(id, sequence) { |
|
||||||
insert_seq.execute(¶ms)?; |
|
||||||
} |
|
||||||
drop(insert_seq); |
|
||||||
} |
|
||||||
save.commit()?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod test { |
|
||||||
use super::*; |
|
||||||
use crate::create_migrations; |
|
||||||
use rusqlite::Connection; |
|
||||||
use sprinklers_core::schedule::Duration; |
|
||||||
use std::sync::Arc; |
|
||||||
use tracing::{debug, trace}; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_update_program() { |
|
||||||
let mut db_conn = Connection::open_in_memory().unwrap(); |
|
||||||
|
|
||||||
let migs = create_migrations(); |
|
||||||
migs.apply(&mut db_conn).unwrap(); |
|
||||||
|
|
||||||
let mut programs = query_programs(&db_conn).unwrap(); |
|
||||||
|
|
||||||
// HACK: ideally ordmap would have iter_mut()
|
|
||||||
let program_ids: Vec<_> = programs.keys().cloned().collect(); |
|
||||||
let mut trans = db_conn.transaction().unwrap(); |
|
||||||
for prog_id in program_ids { |
|
||||||
let prog = &mut programs[&prog_id]; |
|
||||||
let prog = Arc::make_mut(prog); |
|
||||||
debug!(program = debug(&prog), "read program"); |
|
||||||
for _ in 0..1000 { |
|
||||||
let mut schedule = prog.schedule.clone(); |
|
||||||
if let Some(time) = schedule.times.get_mut(0) { |
|
||||||
*time += Duration::seconds(5); |
|
||||||
} |
|
||||||
let mut sequence = prog.sequence.clone(); |
|
||||||
for item in &mut sequence { |
|
||||||
item.duration += std::time::Duration::from_secs(1); |
|
||||||
} |
|
||||||
let prog_update = ProgramUpdateData { |
|
||||||
schedule: Some(schedule), |
|
||||||
sequence: Some(sequence), |
|
||||||
..ProgramUpdateData::default() |
|
||||||
}; |
|
||||||
trace!("about to update"); |
|
||||||
update_program(&mut trans, prog.id, &prog_update).unwrap(); |
|
||||||
trace!("updated, now querying"); |
|
||||||
*prog = query_program_by_id(&*trans, prog.id).unwrap(); |
|
||||||
trace!("updated program: {:?}", &prog); |
|
||||||
} |
|
||||||
debug!("updated program: {:?}", &prog); |
|
||||||
} |
|
||||||
trans.commit().unwrap(); |
|
||||||
debug!("committed final transaction"); |
|
||||||
} |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
use rusqlite::{ |
|
||||||
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef}, |
|
||||||
ToSql, |
|
||||||
}; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
|
|
||||||
pub struct SqlJson<T>(pub T); |
|
||||||
|
|
||||||
impl<T> SqlJson<T> { |
|
||||||
pub fn into_inner(self) -> T { |
|
||||||
self.0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> FromSql for SqlJson<T> |
|
||||||
where |
|
||||||
for<'de> T: Deserialize<'de>, |
|
||||||
{ |
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { |
|
||||||
if let ValueRef::Text(text) = value { |
|
||||||
let deser_value: T = |
|
||||||
serde_json::from_slice(text).map_err(|err| FromSqlError::Other(Box::new(err)))?; |
|
||||||
Ok(SqlJson(deser_value)) |
|
||||||
} else { |
|
||||||
Err(FromSqlError::InvalidType) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> ToSql for SqlJson<T> |
|
||||||
where |
|
||||||
T: Serialize, |
|
||||||
{ |
|
||||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> { |
|
||||||
serde_json::to_string(&self.0) |
|
||||||
.map(|serialized| ToSqlOutput::Owned(Value::Text(serialized))) |
|
||||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err))) |
|
||||||
} |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
use sprinklers_core::model::Zone; |
|
||||||
|
|
||||||
use rusqlite::{Error as SqlError, Row as SqlRow, ToSql}; |
|
||||||
|
|
||||||
pub fn from_sql<'a>(row: &SqlRow<'a>) -> Result<Zone, SqlError> { |
|
||||||
Ok(Zone { |
|
||||||
id: row.get(0)?, |
|
||||||
name: row.get(1)?, |
|
||||||
interface_id: row.get(2)?, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(dead_code)] |
|
||||||
pub fn as_sql(zone: &Zone) -> Vec<&dyn ToSql> { |
|
||||||
vec![&zone.id, &zone.name, &zone.interface_id] |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_linux" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
sprinklers_core = { path = "../sprinklers_core" } |
|
||||||
gpio-cdev = "0.4.0" |
|
||||||
tracing = "0.1.21" |
|
||||||
serde = { version = "1.0.116", features = ["derive"] } |
|
||||||
eyre = "0.6.0" |
|
@ -1,75 +0,0 @@ |
|||||||
use sprinklers_core::zone_interface::{ZoneInterface, ZoneNum}; |
|
||||||
|
|
||||||
use eyre::WrapErr; |
|
||||||
use gpio_cdev::{LineHandle, LineRequestFlags}; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use tracing::{error, trace, warn}; |
|
||||||
|
|
||||||
pub struct LinuxGpio { |
|
||||||
lines: Vec<LineHandle>, |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneInterface for LinuxGpio { |
|
||||||
fn num_zones(&self) -> ZoneNum { |
|
||||||
self.lines.len() as ZoneNum |
|
||||||
} |
|
||||||
|
|
||||||
fn set_zone_state(&self, id: ZoneNum, running: bool) { |
|
||||||
if let Some(line) = &self.lines.get(id as usize) { |
|
||||||
trace!( |
|
||||||
line = line.line().offset(), |
|
||||||
id, |
|
||||||
running, |
|
||||||
"setting state of line" |
|
||||||
); |
|
||||||
if let Err(err) = line.set_value(running as u8) { |
|
||||||
error!("error setting GPIO line value: {}", err); |
|
||||||
} |
|
||||||
} else { |
|
||||||
warn!("set_zone_state: invalid zone id: {}", id); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn get_zone_state(&self, id: ZoneNum) -> bool { |
|
||||||
if let Some(line) = &self.lines.get(id as usize) { |
|
||||||
match line.get_value() { |
|
||||||
Ok(active) => active != 0, |
|
||||||
Err(err) => { |
|
||||||
error!("error getting GPIO line value: {}", err); |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
|
||||||
warn!("get_zone_state: invalid zone id: {}", id); |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
||||||
pub struct LinuxGpioConfig { |
|
||||||
chip_path: String, |
|
||||||
line_offsets: Vec<u32>, |
|
||||||
} |
|
||||||
|
|
||||||
impl LinuxGpioConfig { |
|
||||||
pub fn build(self) -> eyre::Result<LinuxGpio> { |
|
||||||
let mut chip = |
|
||||||
gpio_cdev::Chip::new(self.chip_path).wrap_err("could not create gpio_cdev Chip")?; |
|
||||||
let lines: Result<Vec<_>, eyre::Report> = self |
|
||||||
.line_offsets |
|
||||||
.into_iter() |
|
||||||
.map(|line_offset| { |
|
||||||
let line = chip |
|
||||||
.get_line(line_offset) |
|
||||||
.wrap_err("could not get line for chip")?; |
|
||||||
let line_handle = line |
|
||||||
.request(LineRequestFlags::OUTPUT, 0, "sprinklers_rs") |
|
||||||
.wrap_err("could not request line access")?; |
|
||||||
Ok(line_handle) |
|
||||||
}) |
|
||||||
.collect(); |
|
||||||
let lines = lines?; |
|
||||||
Ok(LinuxGpio { lines }) |
|
||||||
} |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_mqtt" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
sprinklers_core = { path = "../sprinklers_core" } |
|
||||||
sprinklers_actors = { path = "../sprinklers_actors" } |
|
||||||
|
|
||||||
actix = { version = "0.10.0", default-features = false } |
|
||||||
eyre = "0.6.0" |
|
||||||
rumqttc = "0.1.0" |
|
||||||
tracing = "0.1.19" |
|
||||||
serde = { version = "1.0.116", features = ["derive", "rc"] } |
|
||||||
serde_json = "1.0.57" |
|
||||||
chrono = "0.4.15" |
|
||||||
num-traits = "0.2.12" |
|
||||||
num-derive = "0.3.2" |
|
||||||
futures-util = { version = "0.3.5", default-features = false, features = ["std", "async-await", "sink"] } |
|
||||||
im = "15.0.0" |
|
||||||
|
|
||||||
[dependencies.tokio] |
|
||||||
version = "0.2.22" |
|
||||||
default-features = false |
|
||||||
features = [] |
|
@ -1,149 +0,0 @@ |
|||||||
use super::{event_loop::EventLoopTask, request, MqttInterface}; |
|
||||||
use actix::{Actor, ActorContext, ActorFuture, AsyncContext, Handler, WrapFuture}; |
|
||||||
use request::{ErrorCode, RequestContext, RequestError, Response, WithRequestId}; |
|
||||||
use tokio::sync::oneshot; |
|
||||||
use tracing::{debug, error, info, trace, warn}; |
|
||||||
|
|
||||||
pub(super) struct MqttActor { |
|
||||||
interface: MqttInterface, |
|
||||||
event_loop: Option<rumqttc::EventLoop>, |
|
||||||
quit_rx: Option<oneshot::Receiver<()>>, |
|
||||||
request_context: RequestContext, |
|
||||||
} |
|
||||||
|
|
||||||
impl MqttActor { |
|
||||||
pub(super) fn new( |
|
||||||
interface: MqttInterface, |
|
||||||
event_loop: rumqttc::EventLoop, |
|
||||||
request_context: RequestContext, |
|
||||||
) -> Self { |
|
||||||
Self { |
|
||||||
interface, |
|
||||||
event_loop: Some(event_loop), |
|
||||||
quit_rx: None, |
|
||||||
request_context, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn handle_request(&mut self, payload: &[u8], ctx: &mut <Self as Actor>::Context) { |
|
||||||
let request_value = |
|
||||||
match serde_json::from_slice::<WithRequestId<serde_json::Value>>(payload) { |
|
||||||
Ok(r) => r, |
|
||||||
Err(err) => { |
|
||||||
warn!("could not deserialize request: {}", err); |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
let rid = request_value.rid; |
|
||||||
let request_fut = |
|
||||||
serde_json::from_value::<request::Request>(request_value.rest).map(|request| { |
|
||||||
debug!(rid, "about to execute request: {:?}", request); |
|
||||||
request.execute(&mut self.request_context) |
|
||||||
}); |
|
||||||
let mut interface = self.interface.clone(); |
|
||||||
let fut = async move { |
|
||||||
let response = match request_fut { |
|
||||||
Ok(request_fut) => request_fut.await, |
|
||||||
Err(deser_err) => RequestError::with_name_and_cause( |
|
||||||
ErrorCode::Parse, |
|
||||||
"could not parse request", |
|
||||||
"request", |
|
||||||
deser_err, |
|
||||||
) |
|
||||||
.into(), |
|
||||||
}; |
|
||||||
match &response { |
|
||||||
Response::Success(res) => { |
|
||||||
debug!(rid, response = display(res), "success response:"); |
|
||||||
} |
|
||||||
Response::Error(err) => { |
|
||||||
debug!(rid, "request error: {}", err); |
|
||||||
} |
|
||||||
}; |
|
||||||
let resp_with_id = WithRequestId::<request::Response> { |
|
||||||
rid, |
|
||||||
rest: response, |
|
||||||
}; |
|
||||||
if let Err(err) = interface.publish_response(resp_with_id).await { |
|
||||||
error!("could not publish request response: {:?}", err); |
|
||||||
} |
|
||||||
}; |
|
||||||
ctx.spawn(fut.into_actor(self)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Actor for MqttActor { |
|
||||||
type Context = actix::Context<Self>; |
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) { |
|
||||||
trace!("MqttActor starting"); |
|
||||||
let event_loop = self.event_loop.take().expect("MqttActor already started"); |
|
||||||
let (quit_tx, quit_rx) = oneshot::channel(); |
|
||||||
ctx.spawn( |
|
||||||
EventLoopTask::new(event_loop, ctx.address(), quit_tx) |
|
||||||
.run() |
|
||||||
.into_actor(self), |
|
||||||
); |
|
||||||
self.quit_rx = Some(quit_rx); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(actix::Message)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
pub(super) struct Quit; |
|
||||||
|
|
||||||
impl Handler<Quit> for MqttActor { |
|
||||||
type Result = actix::ResponseActFuture<Self, ()>; |
|
||||||
fn handle(&mut self, _msg: Quit, _ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let mut interface = self.interface.clone(); |
|
||||||
let quit_rx = self.quit_rx.take().expect("MqttActor has already quit!"); |
|
||||||
let fut = async move { |
|
||||||
interface |
|
||||||
.cancel() |
|
||||||
.await |
|
||||||
.expect("could not cancel MQTT client"); |
|
||||||
let _ = quit_rx.await; |
|
||||||
} |
|
||||||
.into_actor(self) |
|
||||||
.map(|_, _, ctx| ctx.stop()); |
|
||||||
Box::pin(fut) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(actix::Message)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
pub(super) struct Connected; |
|
||||||
|
|
||||||
impl Handler<Connected> for MqttActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Connected, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
info!("MQTT connected"); |
|
||||||
let mut interface = self.interface.clone(); |
|
||||||
let fut = async move { |
|
||||||
let res = interface.publish_connected(true).await; |
|
||||||
let res = res.and(interface.subscribe_requests().await); |
|
||||||
if let Err(err) = res { |
|
||||||
error!("error in connection setup: {:?}", err); |
|
||||||
} |
|
||||||
}; |
|
||||||
ctx.spawn(fut.into_actor(self)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(actix::Message)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
pub(super) struct PubRecieve(pub(super) rumqttc::Publish); |
|
||||||
|
|
||||||
impl Handler<PubRecieve> for MqttActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, msg: PubRecieve, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
let topic = &msg.0.topic; |
|
||||||
if topic == &self.interface.topics.requests() { |
|
||||||
self.handle_request(msg.0.payload.as_ref(), ctx); |
|
||||||
} else { |
|
||||||
warn!("received on unknown topic: {:?}", topic); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,82 +0,0 @@ |
|||||||
use super::actor::{self, MqttActor}; |
|
||||||
use actix::Addr; |
|
||||||
use rumqttc::{Packet, QoS}; |
|
||||||
use std::{collections::HashSet, time::Duration}; |
|
||||||
use tokio::sync::oneshot; |
|
||||||
use tracing::{debug, trace, warn}; |
|
||||||
|
|
||||||
pub(super) struct EventLoopTask { |
|
||||||
event_loop: rumqttc::EventLoop, |
|
||||||
mqtt_addr: Addr<MqttActor>, |
|
||||||
quit_tx: oneshot::Sender<()>, |
|
||||||
unreleased_pubs: HashSet<u16>, |
|
||||||
} |
|
||||||
|
|
||||||
impl EventLoopTask { |
|
||||||
pub(super) fn new( |
|
||||||
event_loop: rumqttc::EventLoop, |
|
||||||
mqtt_addr: Addr<MqttActor>, |
|
||||||
quit_tx: oneshot::Sender<()>, |
|
||||||
) -> Self { |
|
||||||
Self { |
|
||||||
event_loop, |
|
||||||
mqtt_addr, |
|
||||||
quit_tx, |
|
||||||
unreleased_pubs: HashSet::default(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn handle_incoming(&mut self, incoming: Packet) { |
|
||||||
trace!(incoming = debug(&incoming), "MQTT incoming message"); |
|
||||||
#[allow(clippy::single_match)] |
|
||||||
match incoming { |
|
||||||
Packet::ConnAck(_) => { |
|
||||||
self.mqtt_addr.do_send(actor::Connected); |
|
||||||
} |
|
||||||
Packet::Publish(publish) => { |
|
||||||
// Only deliver QoS 2 packets once
|
|
||||||
let deliver = if publish.qos == QoS::ExactlyOnce { |
|
||||||
if self.unreleased_pubs.contains(&publish.pkid) { |
|
||||||
false |
|
||||||
} else { |
|
||||||
self.unreleased_pubs.insert(publish.pkid); |
|
||||||
true |
|
||||||
} |
|
||||||
} else { |
|
||||||
true |
|
||||||
}; |
|
||||||
if deliver { |
|
||||||
self.mqtt_addr.do_send(actor::PubRecieve(publish)); |
|
||||||
} |
|
||||||
} |
|
||||||
Packet::PubRel(pubrel) => { |
|
||||||
self.unreleased_pubs.remove(&pubrel.pkid); |
|
||||||
} |
|
||||||
_ => {} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub(super) async fn run(mut self) { |
|
||||||
use rumqttc::{ConnectionError, Event}; |
|
||||||
let reconnect_timeout = Duration::from_secs(5); |
|
||||||
self.event_loop.set_reconnection_delay(reconnect_timeout); |
|
||||||
loop { |
|
||||||
match self.event_loop.poll().await { |
|
||||||
Ok(Event::Incoming(incoming)) => { |
|
||||||
self.handle_incoming(incoming); |
|
||||||
} |
|
||||||
Ok(Event::Outgoing(outgoing)) => { |
|
||||||
trace!(outgoing = debug(&outgoing), "MQTT outgoing message"); |
|
||||||
} |
|
||||||
Err(ConnectionError::Cancel) => { |
|
||||||
debug!("MQTT disconnecting"); |
|
||||||
break; |
|
||||||
} |
|
||||||
Err(err) => { |
|
||||||
warn!("MQTT error, reconnecting: {}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
let _ = self.quit_tx.send(()); |
|
||||||
} |
|
||||||
} |
|
@ -1,255 +0,0 @@ |
|||||||
mod actor; |
|
||||||
mod event_loop; |
|
||||||
mod request; |
|
||||||
mod topics; |
|
||||||
mod update_listener; |
|
||||||
mod zone_runner_json; |
|
||||||
|
|
||||||
pub use request::RequestContext; |
|
||||||
pub use update_listener::UpdateListener; |
|
||||||
|
|
||||||
use self::topics::{CollectionTopics, Topics}; |
|
||||||
use sprinklers_actors::zone_runner::ZoneRunnerState; |
|
||||||
use sprinklers_core::model::{ProgramId, Programs, ZoneId, Zones}; |
|
||||||
use zone_runner_json::ZoneRunnerStateJson; |
|
||||||
|
|
||||||
use actix::{Actor, Addr}; |
|
||||||
use eyre::WrapErr; |
|
||||||
use rumqttc::{LastWill, MqttOptions, QoS}; |
|
||||||
use std::{ |
|
||||||
marker::PhantomData, |
|
||||||
ops::{Deref, DerefMut}, |
|
||||||
sync::Arc, |
|
||||||
}; |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct Options { |
|
||||||
pub broker_host: String, |
|
||||||
pub broker_port: u16, |
|
||||||
pub device_id: String, |
|
||||||
pub client_id: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
pub struct MqttInterface { |
|
||||||
client: rumqttc::AsyncClient, |
|
||||||
topics: Topics<Arc<str>>, |
|
||||||
} |
|
||||||
|
|
||||||
impl MqttInterface { |
|
||||||
fn new(options: Options) -> (Self, rumqttc::EventLoop) { |
|
||||||
let mqtt_prefix = format!("devices/{}", options.device_id); |
|
||||||
let topics: Topics<Arc<str>> = Topics::new(mqtt_prefix.into()); |
|
||||||
let mut mqtt_opts = |
|
||||||
MqttOptions::new(options.client_id, options.broker_host, options.broker_port); |
|
||||||
|
|
||||||
let last_will = LastWill::new(topics.connected(), "false", QoS::AtLeastOnce, true); |
|
||||||
mqtt_opts.set_last_will(last_will); |
|
||||||
|
|
||||||
let (client, event_loop) = rumqttc::AsyncClient::new(mqtt_opts, 16); |
|
||||||
|
|
||||||
(Self { client, topics }, event_loop) |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish_data<P>(&mut self, topic: String, payload: &P) -> eyre::Result<()> |
|
||||||
where |
|
||||||
P: serde::Serialize, |
|
||||||
{ |
|
||||||
let payload_vec = |
|
||||||
serde_json::to_vec(payload).wrap_err("failed to serialize publish payload")?; |
|
||||||
self.client |
|
||||||
.publish(topic, QoS::AtLeastOnce, true, payload_vec) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish")?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish_connected(&mut self, connected: bool) -> eyre::Result<()> { |
|
||||||
self.publish_data(self.topics.connected(), &connected) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish connected topic") |
|
||||||
} |
|
||||||
|
|
||||||
async fn cancel(&mut self) -> Result<(), rumqttc::ClientError> { |
|
||||||
self.client.cancel().await |
|
||||||
} |
|
||||||
|
|
||||||
pub fn zones(&mut self) -> MqttCollection<'_, topics::ZoneTopics, Zones> { |
|
||||||
MqttCollection::new(self) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn programs(&mut self) -> MqttCollection<'_, topics::ProgramTopics, Programs> { |
|
||||||
MqttCollection::new(self) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn publish_zone_runner(&mut self, sr_state: &ZoneRunnerState) -> eyre::Result<()> { |
|
||||||
let json: ZoneRunnerStateJson = sr_state.into(); |
|
||||||
self.publish_data(self.topics.zone_runner(), &json) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish zone runner") |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish_response(&mut self, resp: request::ResponseWithId) -> eyre::Result<()> { |
|
||||||
let payload_vec = |
|
||||||
serde_json::to_vec(&resp).wrap_err("failed to serialize request response")?; |
|
||||||
// TODO: if couldn't serialize, just in case can have a static response
|
|
||||||
self.client |
|
||||||
.publish(self.topics.responses(), QoS::AtMostOnce, false, payload_vec) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish request response")?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn subscribe_requests(&mut self) -> eyre::Result<()> { |
|
||||||
self.client |
|
||||||
.subscribe(self.topics.requests(), QoS::ExactlyOnce) |
|
||||||
.await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct MqttCollection<'a, T, U> { |
|
||||||
client: &'a mut rumqttc::AsyncClient, |
|
||||||
topics: T, |
|
||||||
collection: PhantomData<U>, |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a, T: CollectionTopics<'a>, U> MqttCollection<'a, T, U> { |
|
||||||
fn new(interface: &'a mut MqttInterface) -> Self { |
|
||||||
Self { |
|
||||||
client: &mut interface.client, |
|
||||||
topics: T::new(interface.topics.prefix()), |
|
||||||
collection: PhantomData, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish<P: serde::Serialize>( |
|
||||||
&mut self, |
|
||||||
topic: String, |
|
||||||
payload: &P, |
|
||||||
) -> eyre::Result<()> { |
|
||||||
let payload_vec = |
|
||||||
serde_json::to_vec(payload).wrap_err("failed to serialize publish payload")?; |
|
||||||
self.client |
|
||||||
.publish(topic, QoS::AtLeastOnce, true, payload_vec) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish")?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish_ids_impl(&mut self, ids: &[u32]) -> eyre::Result<()> { |
|
||||||
self.publish(self.topics.ids(), &ids).await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn publish_data<V: serde::Serialize>(&mut self, id: u32, item: &V) -> eyre::Result<()> { |
|
||||||
self.publish(self.topics.data(id), item).await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a, T: CollectionTopics<'a>, V: serde::Serialize> |
|
||||||
MqttCollection<'a, T, im::OrdMap<u32, Arc<V>>> |
|
||||||
{ |
|
||||||
async fn publish_ids(&mut self, items: &im::OrdMap<u32, Arc<V>>) -> eyre::Result<()> { |
|
||||||
let ids: Vec<u32> = items.keys().cloned().collect(); |
|
||||||
self.publish_ids_impl(&ids).await |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn publish_diff( |
|
||||||
&mut self, |
|
||||||
old_values: Option<&im::OrdMap<u32, Arc<V>>>, |
|
||||||
new_values: &im::OrdMap<u32, Arc<V>>, |
|
||||||
) -> eyre::Result<()> { |
|
||||||
let mut published_ids = false; |
|
||||||
for (id, value) in new_values { |
|
||||||
let new_value_different = old_values |
|
||||||
.and_then(|old_values| old_values.get(id)) |
|
||||||
.map(|old_value| !Arc::ptr_eq(old_value, value)); |
|
||||||
let publish_value = if let Some(different) = new_value_different { |
|
||||||
different |
|
||||||
} else { |
|
||||||
// old value does not exist
|
|
||||||
if !published_ids { |
|
||||||
self.publish_ids(new_values).await?; |
|
||||||
published_ids = true; |
|
||||||
} |
|
||||||
true |
|
||||||
}; |
|
||||||
if publish_value { |
|
||||||
self.publish_data(*id, &**value).await?; |
|
||||||
} |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn publish_all(&mut self, values: &im::OrdMap<u32, Arc<V>>) -> eyre::Result<()> { |
|
||||||
self.publish_diff(None, values).await |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> MqttCollection<'a, topics::ZoneTopics<'a>, Zones> { |
|
||||||
// Zone state can be derived from zone runner state...
|
|
||||||
pub async fn publish_state(&mut self, zone_id: ZoneId, state: bool) -> eyre::Result<()> { |
|
||||||
self.publish(self.topics.state(zone_id), &state) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish zone state") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> MqttCollection<'a, topics::ProgramTopics<'a>, Programs> { |
|
||||||
pub async fn publish_running( |
|
||||||
&mut self, |
|
||||||
program_id: ProgramId, |
|
||||||
running: bool, |
|
||||||
) -> eyre::Result<()> { |
|
||||||
self.publish(self.topics.running(program_id), &running) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish program running") |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn publish_next_run( |
|
||||||
&mut self, |
|
||||||
program_id: ProgramId, |
|
||||||
next_run: chrono::DateTime<chrono::Local>, |
|
||||||
) -> eyre::Result<()> { |
|
||||||
let payload = next_run.to_rfc3339(); |
|
||||||
self.publish(self.topics.next_run(program_id), &payload) |
|
||||||
.await |
|
||||||
.wrap_err("failed to publish program next run") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct MqttInterfaceTask { |
|
||||||
interface: MqttInterface, |
|
||||||
addr: Addr<actor::MqttActor>, |
|
||||||
} |
|
||||||
|
|
||||||
impl MqttInterfaceTask { |
|
||||||
pub fn start(options: Options, request_context: RequestContext) -> Self { |
|
||||||
let (interface, event_loop) = MqttInterface::new(options); |
|
||||||
|
|
||||||
let addr = actor::MqttActor::new(interface.clone(), event_loop, request_context).start(); |
|
||||||
|
|
||||||
Self { interface, addr } |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn quit(self) -> eyre::Result<()> { |
|
||||||
self.addr.send(actor::Quit).await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Deref for MqttInterfaceTask { |
|
||||||
type Target = MqttInterface; |
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { |
|
||||||
&self.interface |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl DerefMut for MqttInterfaceTask { |
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { |
|
||||||
&mut self.interface |
|
||||||
} |
|
||||||
} |
|
@ -1,297 +0,0 @@ |
|||||||
use sprinklers_actors::{ProgramRunner, StateManager, ZoneRunner}; |
|
||||||
use sprinklers_core::model::Zones; |
|
||||||
|
|
||||||
use futures_util::{ready, FutureExt}; |
|
||||||
use num_derive::FromPrimitive; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::{fmt, future::Future, pin::Pin, task::Poll}; |
|
||||||
use tokio::sync::watch; |
|
||||||
|
|
||||||
mod programs; |
|
||||||
mod zones; |
|
||||||
|
|
||||||
pub struct RequestContext { |
|
||||||
pub zones: watch::Receiver<Zones>, |
|
||||||
pub zone_runner: ZoneRunner, |
|
||||||
pub program_runner: ProgramRunner, |
|
||||||
pub state_manager: StateManager, |
|
||||||
} |
|
||||||
|
|
||||||
type BoxFuture<Output> = Pin<Box<dyn Future<Output = Output>>>; |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] |
|
||||||
#[repr(u16)] |
|
||||||
pub enum ErrorCode { |
|
||||||
BadRequest = 100, |
|
||||||
NotSpecified = 101, |
|
||||||
Parse = 102, |
|
||||||
Range = 103, |
|
||||||
InvalidData = 104, |
|
||||||
BadToken = 105, |
|
||||||
Unauthorized = 106, |
|
||||||
NoPermission = 107, |
|
||||||
NotFound = 109, |
|
||||||
// NotUnique = 110,
|
|
||||||
NoSuchZone = 120, |
|
||||||
NoSuchZoneRun = 121, |
|
||||||
NoSuchProgram = 122, |
|
||||||
Internal = 200, |
|
||||||
NotImplemented = 201, |
|
||||||
Timeout = 300, |
|
||||||
// ServerDisconnected = 301,
|
|
||||||
// BrokerDisconnected = 302,
|
|
||||||
} |
|
||||||
|
|
||||||
mod ser { |
|
||||||
use super::ErrorCode; |
|
||||||
use num_traits::FromPrimitive; |
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
|
||||||
|
|
||||||
impl Serialize for ErrorCode { |
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
|
||||||
where |
|
||||||
S: Serializer, |
|
||||||
{ |
|
||||||
serializer.serialize_u16(*self as u16) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for ErrorCode { |
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
|
||||||
where |
|
||||||
D: Deserializer<'de>, |
|
||||||
{ |
|
||||||
let prim = u16::deserialize(deserializer)?; |
|
||||||
ErrorCode::from_u16(prim) |
|
||||||
.ok_or_else(|| <D::Error as serde::de::Error>::custom("invalid ErrorCode")) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase", tag = "result")] |
|
||||||
pub struct RequestError { |
|
||||||
code: ErrorCode, |
|
||||||
message: String, |
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")] |
|
||||||
name: Option<String>, |
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")] |
|
||||||
cause: Option<String>, |
|
||||||
} |
|
||||||
|
|
||||||
impl fmt::Display for RequestError { |
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
||||||
write!(f, "request error (code {:?}", self.code)?; |
|
||||||
if let Some(name) = &self.name { |
|
||||||
write!(f, "on {}", name)?; |
|
||||||
} |
|
||||||
write!(f, "): {}", self.message)?; |
|
||||||
if let Some(cause) = &self.cause { |
|
||||||
write!(f, ", caused by {}", cause)?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl std::error::Error for RequestError {} |
|
||||||
|
|
||||||
impl From<eyre::Report> for RequestError { |
|
||||||
fn from(report: eyre::Report) -> Self { |
|
||||||
let mut chain = report.chain(); |
|
||||||
let message = match chain.next() { |
|
||||||
Some(a) => a.to_string(), |
|
||||||
None => "unknown error".to_string(), |
|
||||||
}; |
|
||||||
let cause = chain.fold(None, |cause, err| match cause { |
|
||||||
Some(cause) => Some(format!("{}: {}", cause, err)), |
|
||||||
None => Some(err.to_string()), |
|
||||||
}); |
|
||||||
RequestError::new(ErrorCode::Internal, message, None, cause) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[allow(dead_code)] |
|
||||||
impl RequestError { |
|
||||||
pub fn new<M>(code: ErrorCode, message: M, name: Option<String>, cause: Option<String>) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
{ |
|
||||||
Self { |
|
||||||
code, |
|
||||||
message: message.to_string(), |
|
||||||
name, |
|
||||||
cause, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn simple<M>(code: ErrorCode, message: M) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
{ |
|
||||||
Self::new(code, message, None, None) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn with_name<M, N>(code: ErrorCode, message: M, name: N) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
N: ToString, |
|
||||||
{ |
|
||||||
Self::new(code, message, Some(name.to_string()), None) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn with_cause<M, C>(code: ErrorCode, message: M, cause: C) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
C: ToString, |
|
||||||
{ |
|
||||||
Self::new(code, message, None, Some(cause.to_string())) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn with_name_and_cause<M, N, C>(code: ErrorCode, message: M, name: N, cause: C) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
N: ToString, |
|
||||||
C: ToString, |
|
||||||
{ |
|
||||||
Self::new( |
|
||||||
code, |
|
||||||
message, |
|
||||||
Some(name.to_string()), |
|
||||||
Some(cause.to_string()), |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
struct ResponseMessage { |
|
||||||
message: String, |
|
||||||
} |
|
||||||
|
|
||||||
impl ResponseMessage { |
|
||||||
fn new<M>(message: M) -> Self |
|
||||||
where |
|
||||||
M: ToString, |
|
||||||
{ |
|
||||||
ResponseMessage { |
|
||||||
message: message.to_string(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl From<String> for ResponseMessage { |
|
||||||
fn from(message: String) -> Self { |
|
||||||
ResponseMessage { message } |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub type ResponseValue = serde_json::Value; |
|
||||||
|
|
||||||
type RequestResult<Ok = ResponseValue> = Result<Ok, RequestError>; |
|
||||||
type RequestFuture<Ok = ResponseValue> = BoxFuture<RequestResult<Ok>>; |
|
||||||
|
|
||||||
trait IRequest { |
|
||||||
type Response: Serialize; |
|
||||||
|
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response>; |
|
||||||
|
|
||||||
fn exec_erased(self, ctx: &mut RequestContext) -> RequestFuture |
|
||||||
where |
|
||||||
Self::Response: 'static, |
|
||||||
Self: Sized, |
|
||||||
{ |
|
||||||
// TODO: figure out how to get rid of this nested box
|
|
||||||
Box::pin(ErasedRequestFuture(self.exec(ctx))) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
struct ErasedRequestFuture<Response>(RequestFuture<Response>) |
|
||||||
where |
|
||||||
Response: Serialize; |
|
||||||
|
|
||||||
impl<Response> Future for ErasedRequestFuture<Response> |
|
||||||
where |
|
||||||
Response: Serialize, |
|
||||||
{ |
|
||||||
type Output = RequestResult; |
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> { |
|
||||||
use eyre::WrapErr; |
|
||||||
let response = ready!(self.as_mut().0.poll_unpin(cx)); |
|
||||||
Poll::Ready(response.and_then(|res| { |
|
||||||
serde_json::to_value(res) |
|
||||||
.wrap_err("could not serialize response") |
|
||||||
.map_err(RequestError::from) |
|
||||||
})) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase", tag = "result")] |
|
||||||
pub enum Response { |
|
||||||
Success(ResponseValue), |
|
||||||
Error(RequestError), |
|
||||||
} |
|
||||||
|
|
||||||
impl From<RequestResult> for Response { |
|
||||||
fn from(res: RequestResult) -> Self { |
|
||||||
match res { |
|
||||||
Ok(value) => Response::Success(value), |
|
||||||
Err(error) => Response::Error(error), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl From<RequestError> for Response { |
|
||||||
fn from(error: RequestError) -> Self { |
|
||||||
Response::Error(error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct WithRequestId<T> { |
|
||||||
pub rid: i32, |
|
||||||
#[serde(flatten)] |
|
||||||
pub rest: T, |
|
||||||
} |
|
||||||
|
|
||||||
pub type ResponseWithId = WithRequestId<Response>; |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase", tag = "type")] |
|
||||||
pub enum Request { |
|
||||||
// TODO: update nomenclature
|
|
||||||
#[serde(rename = "runSection")] |
|
||||||
RunZone(zones::RunZoneRequest), |
|
||||||
#[serde(rename = "cancelSection")] |
|
||||||
CancelZone(zones::CancelZoneRequest), |
|
||||||
#[serde(rename = "cancelSectionRunId")] |
|
||||||
CancelZoneRunId(zones::CancelZoneRunIdRequest), |
|
||||||
#[serde(rename = "pauseSectionRunner")] |
|
||||||
PauseZoneRunner(zones::PauseZoneRunnerRequest), |
|
||||||
RunProgram(programs::RunProgramRequest), |
|
||||||
CancelProgram(programs::CancelProgramRequest), |
|
||||||
UpdateProgram(programs::UpdateProgramRequest), |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for Request { |
|
||||||
type Response = ResponseValue; |
|
||||||
|
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture { |
|
||||||
match self { |
|
||||||
Request::RunZone(req) => req.exec_erased(ctx), |
|
||||||
Request::CancelZone(req) => req.exec_erased(ctx), |
|
||||||
Request::CancelZoneRunId(req) => req.exec_erased(ctx), |
|
||||||
Request::PauseZoneRunner(req) => req.exec_erased(ctx), |
|
||||||
Request::RunProgram(req) => req.exec_erased(ctx), |
|
||||||
Request::CancelProgram(req) => req.exec_erased(ctx), |
|
||||||
Request::UpdateProgram(req) => req.exec_erased(ctx), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Request { |
|
||||||
pub fn execute(self, ctx: &mut RequestContext) -> impl Future<Output = Response> { |
|
||||||
self.exec(ctx).map(Response::from) |
|
||||||
} |
|
||||||
} |
|
@ -1,106 +0,0 @@ |
|||||||
use super::*; |
|
||||||
use sprinklers_actors::{program_runner::Error, state_manager::StateError}; |
|
||||||
use sprinklers_core::model::{ProgramId, ProgramRef, ProgramUpdateData}; |
|
||||||
|
|
||||||
use eyre::WrapErr; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct RunProgramRequest { |
|
||||||
program_id: ProgramId, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for RunProgramRequest { |
|
||||||
type Response = ResponseMessage; |
|
||||||
|
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut program_runner = ctx.program_runner.clone(); |
|
||||||
let program_id = self.program_id; |
|
||||||
Box::pin(async move { |
|
||||||
match program_runner.run_program_id(program_id).await { |
|
||||||
Ok(program) => Ok(ResponseMessage::new(format!( |
|
||||||
"running program '{}'", |
|
||||||
program.name |
|
||||||
))), |
|
||||||
Err(e @ Error::InvalidProgramId(_)) => Err(RequestError::with_name( |
|
||||||
ErrorCode::NoSuchProgram, |
|
||||||
e, |
|
||||||
"program", |
|
||||||
)), |
|
||||||
Err(e) => Err(e).wrap_err("could not run program")?, |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct CancelProgramRequest { |
|
||||||
program_id: ProgramId, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for CancelProgramRequest { |
|
||||||
type Response = ResponseMessage; |
|
||||||
|
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut program_runner = ctx.program_runner.clone(); |
|
||||||
let program_id = self.program_id; |
|
||||||
Box::pin(async move { |
|
||||||
let cancelled = program_runner |
|
||||||
.cancel_program(program_id) |
|
||||||
.await |
|
||||||
.wrap_err("could not run cancel program")?; |
|
||||||
match cancelled { |
|
||||||
Some(program) => Ok(ResponseMessage::new(format!( |
|
||||||
"program '{}' cancelled", |
|
||||||
program.name |
|
||||||
))), |
|
||||||
None => Err(RequestError::with_name( |
|
||||||
ErrorCode::NoSuchProgram, |
|
||||||
"program was not running", |
|
||||||
"program", |
|
||||||
)), |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct UpdateProgramRequest { |
|
||||||
program_id: ProgramId, |
|
||||||
data: ProgramUpdateData, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct UpdateProgramResponse { |
|
||||||
message: String, |
|
||||||
data: ProgramRef, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for UpdateProgramRequest { |
|
||||||
type Response = UpdateProgramResponse; |
|
||||||
|
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut state_manager = ctx.state_manager.clone(); |
|
||||||
Box::pin(async move { |
|
||||||
let new_program = state_manager |
|
||||||
.update_program(self.program_id, self.data) |
|
||||||
.await |
|
||||||
.map_err(|err| match err { |
|
||||||
e @ StateError::NoSuchProgram(_) => RequestError::with_name_and_cause( |
|
||||||
ErrorCode::NoSuchProgram, |
|
||||||
"could not update program", |
|
||||||
"program", |
|
||||||
e, |
|
||||||
), |
|
||||||
e => RequestError::from(eyre::Report::from(e)), |
|
||||||
})?; |
|
||||||
Ok(UpdateProgramResponse { |
|
||||||
message: format!("updated program '{}'", new_program.name), |
|
||||||
data: new_program, |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -1,160 +0,0 @@ |
|||||||
use super::*; |
|
||||||
use sprinklers_actors::zone_runner::ZoneRunHandle; |
|
||||||
use sprinklers_core::model::{self, ZoneRef}; |
|
||||||
use sprinklers_core::serde::duration_secs; |
|
||||||
|
|
||||||
use eyre::WrapErr; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::time::Duration; |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] |
|
||||||
#[serde(transparent)] |
|
||||||
pub struct ZoneId(pub model::ZoneId); |
|
||||||
|
|
||||||
impl ZoneId { |
|
||||||
fn get_zone(self, zones: &Zones) -> Result<ZoneRef, RequestError> { |
|
||||||
zones |
|
||||||
.get(&self.0) |
|
||||||
.cloned() |
|
||||||
.ok_or_else(|| RequestError::with_name(ErrorCode::NoSuchZone, "no such zone", "zone")) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct RunZoneRequest { |
|
||||||
// TODO: update nomenclature
|
|
||||||
#[serde(rename = "sectionId")] |
|
||||||
pub zone_id: ZoneId, |
|
||||||
#[serde(with = "duration_secs")] |
|
||||||
pub duration: Duration, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct RunZoneResponse { |
|
||||||
pub message: String, |
|
||||||
pub run_id: ZoneRunHandle, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for RunZoneRequest { |
|
||||||
type Response = RunZoneResponse; |
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut zone_runner = ctx.zone_runner.clone(); |
|
||||||
let zone = self.zone_id.get_zone(&*ctx.zones.borrow()); |
|
||||||
let duration = self.duration; |
|
||||||
Box::pin(async move { |
|
||||||
let zone = zone?; |
|
||||||
let handle = zone_runner |
|
||||||
.queue_run(zone.clone(), duration) |
|
||||||
.await |
|
||||||
.wrap_err("could not queue run")?; |
|
||||||
Ok(RunZoneResponse { |
|
||||||
message: format!("running zone '{}' for {:?}", &zone.name, duration), |
|
||||||
run_id: handle, |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct CancelZoneRequest { |
|
||||||
// TODO: update nomenclature
|
|
||||||
#[serde(rename = "sectionId")] |
|
||||||
pub zone_id: ZoneId, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct CancelZoneResponse { |
|
||||||
pub message: String, |
|
||||||
pub cancelled: usize, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for CancelZoneRequest { |
|
||||||
type Response = CancelZoneResponse; |
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut zone_runner = ctx.zone_runner.clone(); |
|
||||||
let zone = self.zone_id.get_zone(&*ctx.zones.borrow()); |
|
||||||
Box::pin(async move { |
|
||||||
let zone = zone?; |
|
||||||
let cancelled = zone_runner |
|
||||||
.cancel_by_zone(zone.id) |
|
||||||
.await |
|
||||||
.wrap_err("could not cancel zone")?; |
|
||||||
Ok(CancelZoneResponse { |
|
||||||
message: format!("cancelled {} runs for zone '{}'", cancelled, zone.name), |
|
||||||
cancelled, |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct CancelZoneRunIdRequest { |
|
||||||
pub run_id: ZoneRunHandle, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct CancelZoneRunIdResponse { |
|
||||||
pub message: String, |
|
||||||
pub cancelled: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for CancelZoneRunIdRequest { |
|
||||||
type Response = ResponseMessage; |
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut zone_runner = ctx.zone_runner.clone(); |
|
||||||
Box::pin(async move { |
|
||||||
let cancelled = zone_runner |
|
||||||
.cancel_run(self.run_id) |
|
||||||
.await |
|
||||||
.wrap_err("could not cancel zone run")?; |
|
||||||
if cancelled { |
|
||||||
Ok(ResponseMessage::new("cancelled zone run")) |
|
||||||
} else { |
|
||||||
Err(RequestError::with_name( |
|
||||||
ErrorCode::NoSuchZoneRun, |
|
||||||
"no such zone run", |
|
||||||
"zone run", |
|
||||||
)) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct PauseZoneRunnerRequest { |
|
||||||
pub paused: bool, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct PauseZoneRunnerResponse { |
|
||||||
pub message: String, |
|
||||||
pub paused: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl IRequest for PauseZoneRunnerRequest { |
|
||||||
type Response = PauseZoneRunnerResponse; |
|
||||||
fn exec(self, ctx: &mut RequestContext) -> RequestFuture<Self::Response> { |
|
||||||
let mut zone_runner = ctx.zone_runner.clone(); |
|
||||||
let paused = self.paused; |
|
||||||
Box::pin(async move { |
|
||||||
if paused { |
|
||||||
zone_runner.pause().await |
|
||||||
} else { |
|
||||||
zone_runner.unpause().await |
|
||||||
} |
|
||||||
.wrap_err("could not pause/unpause zone runner")?; |
|
||||||
Ok(PauseZoneRunnerResponse { |
|
||||||
message: format!("{} zone runner", if paused { "paused" } else { "unpaused" }), |
|
||||||
paused, |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -1,97 +0,0 @@ |
|||||||
pub trait CollectionTopics<'t> { |
|
||||||
fn new(prefix: &'t str) -> Self; |
|
||||||
fn ids(&self) -> String; |
|
||||||
fn data(&self, id: u32) -> String; |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct ZoneTopics<'a>(pub &'a str); |
|
||||||
|
|
||||||
impl<'a> CollectionTopics<'a> for ZoneTopics<'a> { |
|
||||||
fn new(prefix: &'a str) -> Self { |
|
||||||
ZoneTopics(prefix) |
|
||||||
} |
|
||||||
|
|
||||||
fn ids(&self) -> String { |
|
||||||
// TODO: change nomenclature
|
|
||||||
format!("{}/sections", self.0) |
|
||||||
} |
|
||||||
|
|
||||||
fn data(&self, zone_id: u32) -> String { |
|
||||||
// TODO: change nomenclature
|
|
||||||
format!("{}/sections/{}", self.0, zone_id) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> ZoneTopics<'a> { |
|
||||||
pub fn state(&self, zone_id: u32) -> String { |
|
||||||
// TODO: change nomenclature
|
|
||||||
format!("{}/sections/{}/state", self.0, zone_id) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct ProgramTopics<'a>(pub &'a str); |
|
||||||
|
|
||||||
impl<'a> CollectionTopics<'a> for ProgramTopics<'a> { |
|
||||||
fn new(prefix: &'a str) -> Self { |
|
||||||
ProgramTopics(prefix) |
|
||||||
} |
|
||||||
|
|
||||||
fn ids(&self) -> String { |
|
||||||
format!("{}/programs", self.0) |
|
||||||
} |
|
||||||
|
|
||||||
fn data(&self, zone_id: u32) -> String { |
|
||||||
format!("{}/programs/{}", self.0, zone_id) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> ProgramTopics<'a> { |
|
||||||
pub fn running(&self, zone_id: u32) -> String { |
|
||||||
format!("{}/programs/{}/running", self.0, zone_id) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn next_run(&self, zone_id: u32) -> String { |
|
||||||
// TODO: reconcile naming convention
|
|
||||||
format!("{}/programs/{}/nextRun", self.0, zone_id) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct Topics<T: AsRef<str>>(pub T); |
|
||||||
|
|
||||||
impl<T: AsRef<str>> Topics<T> { |
|
||||||
pub fn new(prefix: T) -> Self { |
|
||||||
Self(prefix) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn prefix(&self) -> &str { |
|
||||||
self.0.as_ref() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn connected(&self) -> String { |
|
||||||
format!("{}/connected", self.0.as_ref()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn zones(&self) -> ZoneTopics { |
|
||||||
ZoneTopics::new(self.0.as_ref()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn programs(&self) -> ProgramTopics { |
|
||||||
ProgramTopics::new(self.0.as_ref()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn zone_runner(&self) -> String { |
|
||||||
// TODO: change nomenclature
|
|
||||||
format!("{}/section_runner", self.0.as_ref()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn requests(&self) -> String { |
|
||||||
format!("{}/requests", self.0.as_ref()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn responses(&self) -> String { |
|
||||||
format!("{}/responses", self.0.as_ref()) |
|
||||||
} |
|
||||||
} |
|
@ -1,295 +0,0 @@ |
|||||||
use super::MqttInterface; |
|
||||||
use sprinklers_actors::{ |
|
||||||
program_runner::{ProgramEvent, ProgramEventRecv}, |
|
||||||
zone_runner::{ZoneEvent, ZoneEventRecv, ZoneRunnerState, ZoneRunnerStateRecv}, |
|
||||||
}; |
|
||||||
|
|
||||||
use actix::{fut::wrap_future, Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler}; |
|
||||||
use futures_util::TryFutureExt; |
|
||||||
use sprinklers_core::model::{Programs, Zones}; |
|
||||||
use tokio::sync::{broadcast, watch}; |
|
||||||
use tracing::{trace, warn}; |
|
||||||
|
|
||||||
struct UpdateListenerActor { |
|
||||||
mqtt_interface: MqttInterface, |
|
||||||
old_zones: Option<Zones>, |
|
||||||
old_programs: Option<Programs>, |
|
||||||
} |
|
||||||
|
|
||||||
impl UpdateListenerActor { |
|
||||||
fn new(mqtt_interface: MqttInterface) -> Self { |
|
||||||
Self { |
|
||||||
mqtt_interface, |
|
||||||
old_zones: None, |
|
||||||
old_programs: None, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Actor for UpdateListenerActor { |
|
||||||
type Context = actix::Context<Self>; |
|
||||||
|
|
||||||
fn started(&mut self, _ctx: &mut Self::Context) { |
|
||||||
trace!("starting UpdateListener"); |
|
||||||
} |
|
||||||
|
|
||||||
fn stopped(&mut self, _ctx: &mut Self::Context) { |
|
||||||
trace!("stopped UpdateListener") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl StreamHandler<Zones> for UpdateListenerActor { |
|
||||||
fn handle(&mut self, zones: Zones, ctx: &mut Self::Context) { |
|
||||||
let mut mqtt_interface = self.mqtt_interface.clone(); |
|
||||||
|
|
||||||
let old_zones = self.old_zones.replace(zones.clone()); |
|
||||||
|
|
||||||
let fut = async move { |
|
||||||
if old_zones.is_none() { |
|
||||||
// Some what of a hack
|
|
||||||
// Initialize zone running states to false the first time we
|
|
||||||
// receive zones
|
|
||||||
for zone_id in zones.keys() { |
|
||||||
mqtt_interface |
|
||||||
.zones() |
|
||||||
.publish_state(*zone_id, false) |
|
||||||
.await?; |
|
||||||
} |
|
||||||
} |
|
||||||
mqtt_interface |
|
||||||
.zones() |
|
||||||
.publish_diff(old_zones.as_ref(), &zones) |
|
||||||
.await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
.unwrap_or_else(|err: eyre::Report| warn!("could not publish programs: {:?}", err)); |
|
||||||
ctx.spawn(wrap_future(fut)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl StreamHandler<Result<ZoneEvent, broadcast::RecvError>> for UpdateListenerActor { |
|
||||||
fn handle(&mut self, event: Result<ZoneEvent, broadcast::RecvError>, ctx: &mut Self::Context) { |
|
||||||
let event = match event { |
|
||||||
Ok(ev) => ev, |
|
||||||
Err(broadcast::RecvError::Closed) => unreachable!(), |
|
||||||
Err(broadcast::RecvError::Lagged(n)) => { |
|
||||||
warn!("zone events lagged by {}", n); |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
if let Some((zone_id, state)) = match event { |
|
||||||
ZoneEvent::RunStart(_, zone) | ZoneEvent::RunUnpause(_, zone) => Some((zone.id, true)), |
|
||||||
ZoneEvent::RunFinish(_, zone) |
|
||||||
| ZoneEvent::RunPause(_, zone) |
|
||||||
| ZoneEvent::RunCancel(_, zone) => Some((zone.id, false)), |
|
||||||
ZoneEvent::RunnerPause | ZoneEvent::RunnerUnpause => None, |
|
||||||
} { |
|
||||||
let mut mqtt_interface = self.mqtt_interface.clone(); |
|
||||||
let fut = async move { |
|
||||||
if let Err(err) = mqtt_interface.zones().publish_state(zone_id, state).await { |
|
||||||
warn!("could not publish zone state: {}", err); |
|
||||||
} |
|
||||||
}; |
|
||||||
ctx.spawn(wrap_future(fut)); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl StreamHandler<Result<ProgramEvent, broadcast::RecvError>> for UpdateListenerActor { |
|
||||||
fn handle( |
|
||||||
&mut self, |
|
||||||
event: Result<ProgramEvent, broadcast::RecvError>, |
|
||||||
ctx: &mut Self::Context, |
|
||||||
) { |
|
||||||
let event = match event { |
|
||||||
Ok(ev) => ev, |
|
||||||
Err(broadcast::RecvError::Closed) => unreachable!(), |
|
||||||
Err(broadcast::RecvError::Lagged(n)) => { |
|
||||||
warn!("program events lagged by {}", n); |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
let mut mqtt_interface = self.mqtt_interface.clone(); |
|
||||||
let fut = async move { |
|
||||||
enum Publish { |
|
||||||
Running(bool), |
|
||||||
NextRun(chrono::DateTime<chrono::Local>), |
|
||||||
} |
|
||||||
|
|
||||||
let (program_id, publish) = match event { |
|
||||||
ProgramEvent::RunStart(prog) => (prog.id, Publish::Running(true)), |
|
||||||
ProgramEvent::RunFinish(prog) | ProgramEvent::RunCancel(prog) => { |
|
||||||
(prog.id, Publish::Running(false)) |
|
||||||
} |
|
||||||
ProgramEvent::NextRun(prog, next_run) => (prog.id, Publish::NextRun(next_run)), |
|
||||||
}; |
|
||||||
match publish { |
|
||||||
Publish::Running(running) => { |
|
||||||
if let Err(err) = mqtt_interface |
|
||||||
.programs() |
|
||||||
.publish_running(program_id, running) |
|
||||||
.await |
|
||||||
{ |
|
||||||
warn!("could not publish program running: {}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
Publish::NextRun(next_run) => { |
|
||||||
if let Err(err) = mqtt_interface |
|
||||||
.programs() |
|
||||||
.publish_next_run(program_id, next_run) |
|
||||||
.await |
|
||||||
{ |
|
||||||
warn!("could not publish program next run: {}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
ctx.spawn(wrap_future(fut)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl StreamHandler<ZoneRunnerState> for UpdateListenerActor { |
|
||||||
fn handle(&mut self, state: ZoneRunnerState, ctx: &mut Self::Context) { |
|
||||||
let mut mqtt_interface = self.mqtt_interface.clone(); |
|
||||||
let fut = async move { |
|
||||||
if let Err(err) = mqtt_interface.publish_zone_runner(&state).await { |
|
||||||
warn!("could not publish zone runner: {}", err); |
|
||||||
} |
|
||||||
}; |
|
||||||
ctx.spawn(wrap_future(fut)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl StreamHandler<Programs> for UpdateListenerActor { |
|
||||||
fn handle(&mut self, programs: Programs, ctx: &mut Self::Context) { |
|
||||||
let mut mqtt_interface = self.mqtt_interface.clone(); |
|
||||||
|
|
||||||
let old_programs = self.old_programs.replace(programs.clone()); |
|
||||||
|
|
||||||
let fut = async move { |
|
||||||
let mut mqtt_progs = mqtt_interface.programs(); |
|
||||||
if old_programs.is_none() { |
|
||||||
// Some what of a hack
|
|
||||||
// Initialize program running states to false the first time we
|
|
||||||
// receive programs
|
|
||||||
for program_id in programs.keys() { |
|
||||||
mqtt_progs.publish_running(*program_id, false).await?; |
|
||||||
} |
|
||||||
} |
|
||||||
mqtt_progs |
|
||||||
.publish_diff(old_programs.as_ref(), &programs) |
|
||||||
.await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
.unwrap_or_else(|err: eyre::Report| warn!("could not publish programs: {:?}", err)); |
|
||||||
ctx.spawn(wrap_future(fut)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(actix::Message)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct Quit; |
|
||||||
|
|
||||||
impl Handler<Quit> for UpdateListenerActor { |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Quit, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
ctx.stop(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
trait Listenable<A>: Send |
|
||||||
where |
|
||||||
A: Actor, |
|
||||||
{ |
|
||||||
fn listen(self, ctx: &mut A::Context); |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(actix::Message)] |
|
||||||
#[rtype(result = "()")] |
|
||||||
struct Listen<L>(L) |
|
||||||
where |
|
||||||
L: Listenable<UpdateListenerActor>; |
|
||||||
|
|
||||||
impl<L> Handler<Listen<L>> for UpdateListenerActor |
|
||||||
where |
|
||||||
L: Listenable<Self>, |
|
||||||
{ |
|
||||||
type Result = (); |
|
||||||
|
|
||||||
fn handle(&mut self, msg: Listen<L>, ctx: &mut Self::Context) -> Self::Result { |
|
||||||
msg.0.listen(ctx); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Listenable<UpdateListenerActor> for watch::Receiver<Zones> { |
|
||||||
fn listen(self, ctx: &mut <UpdateListenerActor as Actor>::Context) { |
|
||||||
ctx.add_stream(self); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Listenable<UpdateListenerActor> for ZoneEventRecv { |
|
||||||
fn listen(self, ctx: &mut <UpdateListenerActor as Actor>::Context) { |
|
||||||
ctx.add_stream(self); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Listenable<UpdateListenerActor> for ZoneRunnerStateRecv { |
|
||||||
fn listen(self, ctx: &mut <UpdateListenerActor as Actor>::Context) { |
|
||||||
ctx.add_stream(self); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Listenable<UpdateListenerActor> for watch::Receiver<Programs> { |
|
||||||
fn listen(self, ctx: &mut <UpdateListenerActor as Actor>::Context) { |
|
||||||
ctx.add_stream(self); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Listenable<UpdateListenerActor> for ProgramEventRecv { |
|
||||||
fn listen(self, ctx: &mut <UpdateListenerActor as Actor>::Context) { |
|
||||||
ctx.add_stream(self); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct UpdateListener { |
|
||||||
addr: Addr<UpdateListenerActor>, |
|
||||||
} |
|
||||||
|
|
||||||
impl UpdateListener { |
|
||||||
pub fn start(mqtt_interface: MqttInterface) -> Self { |
|
||||||
let addr = UpdateListenerActor::new(mqtt_interface).start(); |
|
||||||
Self { addr } |
|
||||||
} |
|
||||||
|
|
||||||
fn listen<L: 'static>(&mut self, listener: L) |
|
||||||
where |
|
||||||
L: Listenable<UpdateListenerActor>, |
|
||||||
{ |
|
||||||
self.addr.do_send(Listen(listener)); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn listen_zones(&mut self, zones: watch::Receiver<Zones>) { |
|
||||||
self.listen(zones); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn listen_zone_events(&mut self, zone_events: ZoneEventRecv) { |
|
||||||
self.listen(zone_events); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn listen_zone_runner(&mut self, zone_runner_state_recv: ZoneRunnerStateRecv) { |
|
||||||
self.listen(zone_runner_state_recv); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn listen_programs(&mut self, programs: watch::Receiver<Programs>) { |
|
||||||
self.listen(programs); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn listen_program_events(&mut self, program_events: ProgramEventRecv) { |
|
||||||
self.listen(program_events); |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn quit(self) -> eyre::Result<()> { |
|
||||||
Ok(self.addr.send(Quit).await?) |
|
||||||
} |
|
||||||
} |
|
@ -1,76 +0,0 @@ |
|||||||
use sprinklers_actors::zone_runner::{ZoneRun, ZoneRunState, ZoneRunnerState}; |
|
||||||
use sprinklers_core::model::ZoneId; |
|
||||||
|
|
||||||
use chrono::{DateTime, Utc}; |
|
||||||
use serde::Serialize; |
|
||||||
use std::time::SystemTime; |
|
||||||
use tokio::time::Instant; |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct ZoneRunJson { |
|
||||||
id: i32, |
|
||||||
// TODO: change nomenclature
|
|
||||||
#[serde(rename = "section")] |
|
||||||
zone: ZoneId, |
|
||||||
total_duration: f64, |
|
||||||
duration: f64, |
|
||||||
start_time: Option<String>, |
|
||||||
pause_time: Option<String>, |
|
||||||
unpause_time: Option<String>, |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneRunJson { |
|
||||||
fn from_run(run: &ZoneRun) -> Option<Self> { |
|
||||||
let (now, system_now) = (Instant::now(), SystemTime::now()); |
|
||||||
let instant_to_string = |instant: Instant| -> String { |
|
||||||
DateTime::<Utc>::from(system_now - now.duration_since(instant)).to_rfc3339() |
|
||||||
}; |
|
||||||
let (start_time, pause_time) = match run.state { |
|
||||||
ZoneRunState::Finished | ZoneRunState::Cancelled => { |
|
||||||
return None; |
|
||||||
} |
|
||||||
ZoneRunState::Waiting => (None, None), |
|
||||||
ZoneRunState::Running { start_time } => (Some(instant_to_string(start_time)), None), |
|
||||||
ZoneRunState::Paused { |
|
||||||
start_time, |
|
||||||
pause_time, |
|
||||||
} => ( |
|
||||||
Some(instant_to_string(start_time)), |
|
||||||
Some(instant_to_string(pause_time)), |
|
||||||
), |
|
||||||
}; |
|
||||||
Some(Self { |
|
||||||
id: run.handle.clone().into_inner(), |
|
||||||
zone: run.zone.id, |
|
||||||
total_duration: run.total_duration.as_secs_f64(), |
|
||||||
duration: run.duration.as_secs_f64(), |
|
||||||
start_time, |
|
||||||
pause_time, |
|
||||||
unpause_time: None, // TODO: this is kinda useless, should probably be removed
|
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)] |
|
||||||
#[serde(rename_all = "camelCase")] |
|
||||||
pub struct ZoneRunnerStateJson { |
|
||||||
queue: Vec<ZoneRunJson>, |
|
||||||
current: Option<ZoneRunJson>, |
|
||||||
paused: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl From<&ZoneRunnerState> for ZoneRunnerStateJson { |
|
||||||
fn from(state: &ZoneRunnerState) -> Self { |
|
||||||
let mut run_queue = state.run_queue.iter(); |
|
||||||
let current = run_queue.next().and_then(|run| ZoneRunJson::from_run(run)); |
|
||||||
let queue = run_queue |
|
||||||
.filter_map(|run| ZoneRunJson::from_run(run)) |
|
||||||
.collect(); |
|
||||||
Self { |
|
||||||
queue, |
|
||||||
current, |
|
||||||
paused: state.paused, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,33 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "sprinklers_rs" |
|
||||||
version = "0.1.0" |
|
||||||
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
|
||||||
edition = "2018" |
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
|
||||||
|
|
||||||
[features] |
|
||||||
default = ["sprinklers_linux"] |
|
||||||
bundled_sqlite = ["sprinklers_database/bundled"] |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
sprinklers_core = { path = "../sprinklers_core" } |
|
||||||
sprinklers_database = { path = "../sprinklers_database" } |
|
||||||
sprinklers_actors = { path = "../sprinklers_actors" } |
|
||||||
sprinklers_mqtt = { path = "../sprinklers_mqtt" } |
|
||||||
sprinklers_linux = { path = "../sprinklers_linux", optional = true } |
|
||||||
|
|
||||||
color-eyre = "0.5.1" |
|
||||||
eyre = "0.6.0" |
|
||||||
tokio = "0.2.22" |
|
||||||
tracing = { version = "0.1.19" } |
|
||||||
actix = { version = "0.10.0", default-features = false } |
|
||||||
actix-rt = "1.1.1" |
|
||||||
chrono = "0.4.19" |
|
||||||
serde = { version = "1.0.116", features = ["derive"] } |
|
||||||
config = { version = "0.10.1", default-features = false, features = ["json"] } |
|
||||||
|
|
||||||
[dependencies.tracing-subscriber] |
|
||||||
version = "0.2.11" |
|
||||||
default-features = false |
|
||||||
features = ["registry", "fmt", "env-filter", "ansi"] |
|
@ -1,12 +0,0 @@ |
|||||||
{ |
|
||||||
"mqtt": { |
|
||||||
"broker_host": "localhost", |
|
||||||
"broker_port": 1883, |
|
||||||
"client_id": "sprinklers_rs-0001", |
|
||||||
"device_id": "sprinklers_rs-0001" |
|
||||||
}, |
|
||||||
"zone_interface": { |
|
||||||
"provider": "Mock", |
|
||||||
"num_zones": 6 |
|
||||||
} |
|
||||||
} |
|
@ -1,74 +0,0 @@ |
|||||||
#![warn(clippy::all)] |
|
||||||
#![warn(clippy::print_stdout)] |
|
||||||
|
|
||||||
// mod option_future;
|
|
||||||
mod settings; |
|
||||||
mod state_manager; |
|
||||||
mod zone_interface; |
|
||||||
|
|
||||||
use sprinklers_actors as actors; |
|
||||||
use sprinklers_database as database; |
|
||||||
use sprinklers_mqtt as mqtt; |
|
||||||
|
|
||||||
use eyre::{Result, WrapErr}; |
|
||||||
use settings::Settings; |
|
||||||
use tracing::info; |
|
||||||
use tracing_subscriber::EnvFilter; |
|
||||||
|
|
||||||
#[actix_rt::main] |
|
||||||
async fn main() -> Result<()> { |
|
||||||
color_eyre::install()?; |
|
||||||
tracing_subscriber::fmt() |
|
||||||
.with_ansi(true) |
|
||||||
.with_env_filter( |
|
||||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), |
|
||||||
) |
|
||||||
.init(); |
|
||||||
|
|
||||||
let settings: Settings = Settings::new().wrap_err("could not load settings")?; |
|
||||||
|
|
||||||
info!("Starting sprinklers_rs..."); |
|
||||||
|
|
||||||
let db_conn = database::setup_db()?; |
|
||||||
|
|
||||||
let zone_interface = settings.zone_interface.build()?; |
|
||||||
let mut zone_runner = actors::ZoneRunner::new(zone_interface); |
|
||||||
let mut program_runner = actors::ProgramRunner::new(zone_runner.clone()); |
|
||||||
|
|
||||||
let state_manager = crate::state_manager::StateManagerThread::start(db_conn); |
|
||||||
|
|
||||||
let mqtt_options = settings.mqtt; |
|
||||||
// TODO: have ability to update zones / other data
|
|
||||||
let request_context = mqtt::RequestContext { |
|
||||||
zones: state_manager.get_zones(), |
|
||||||
zone_runner: zone_runner.clone(), |
|
||||||
program_runner: program_runner.clone(), |
|
||||||
state_manager: state_manager.clone(), |
|
||||||
}; |
|
||||||
let mqtt_interface = mqtt::MqttInterfaceTask::start(mqtt_options, request_context); |
|
||||||
|
|
||||||
let mut update_listener = mqtt::UpdateListener::start(mqtt_interface.clone()); |
|
||||||
update_listener.listen_zones(state_manager.get_zones()); |
|
||||||
update_listener.listen_zone_events(zone_runner.subscribe().await?); |
|
||||||
update_listener.listen_zone_runner(zone_runner.get_state_recv()); |
|
||||||
update_listener.listen_programs(state_manager.get_programs()); |
|
||||||
update_listener.listen_program_events(program_runner.subscribe().await?); |
|
||||||
|
|
||||||
// Only listen to programs now so above subscriptions get events
|
|
||||||
program_runner.listen_zones(state_manager.get_zones()); |
|
||||||
program_runner.listen_programs(state_manager.get_programs()); |
|
||||||
|
|
||||||
info!("sprinklers_rs initialized"); |
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await?; |
|
||||||
info!("Interrupt received, shutting down"); |
|
||||||
|
|
||||||
update_listener.quit().await?; |
|
||||||
mqtt_interface.quit().await?; |
|
||||||
drop(state_manager); |
|
||||||
program_runner.quit().await?; |
|
||||||
zone_runner.quit().await?; |
|
||||||
actix::System::current().stop(); |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
@ -1,37 +0,0 @@ |
|||||||
use pin_project::pin_project; |
|
||||||
use std::{ |
|
||||||
future::Future, |
|
||||||
ops::Deref, |
|
||||||
pin::Pin, |
|
||||||
task::{Context, Poll}, |
|
||||||
}; |
|
||||||
|
|
||||||
#[pin_project] |
|
||||||
#[derive(Debug, Clone)] |
|
||||||
#[must_use = "futures do nothing unless you `.await` or poll them"] |
|
||||||
pub struct OptionFuture<F>(#[pin] Option<F>); |
|
||||||
|
|
||||||
impl<F: Future> Future for OptionFuture<F> { |
|
||||||
type Output = Option<F::Output>; |
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { |
|
||||||
match self.project().0.as_pin_mut() { |
|
||||||
Some(x) => x.poll(cx).map(Some), |
|
||||||
None => Poll::Ready(None), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<F> Deref for OptionFuture<F> { |
|
||||||
type Target = Option<F>; |
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { |
|
||||||
&self.0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> From<Option<T>> for OptionFuture<T> { |
|
||||||
fn from(option: Option<T>) -> Self { |
|
||||||
OptionFuture(option) |
|
||||||
} |
|
||||||
} |
|
@ -1,44 +0,0 @@ |
|||||||
use crate::zone_interface::ZoneInterfaceConfig; |
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use tracing::trace; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(remote = "sprinklers_mqtt::Options")] |
|
||||||
struct MqttOptions { |
|
||||||
pub broker_host: String, |
|
||||||
pub broker_port: u16, |
|
||||||
pub device_id: String, |
|
||||||
pub client_id: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
pub struct Settings { |
|
||||||
#[serde(with = "MqttOptions")] |
|
||||||
pub mqtt: sprinklers_mqtt::Options, |
|
||||||
#[serde(default)] |
|
||||||
pub zone_interface: ZoneInterfaceConfig, |
|
||||||
} |
|
||||||
|
|
||||||
impl Settings { |
|
||||||
pub fn new() -> eyre::Result<Self> { |
|
||||||
let mut s = config::Config::new(); |
|
||||||
|
|
||||||
let default_config = config::File::from_str( |
|
||||||
include_str!("../sprinklers_rs.default.json"), |
|
||||||
config::FileFormat::Json, |
|
||||||
); |
|
||||||
s.merge(default_config)?; |
|
||||||
|
|
||||||
// TODO: specify configuration path from arguments or env
|
|
||||||
s.merge(config::File::with_name("sprinklers_rs").required(false))?; |
|
||||||
|
|
||||||
s.merge(config::Environment::with_prefix("SPRINKLERS").separator("__"))?; |
|
||||||
|
|
||||||
let settings: Settings = s.try_into()?; |
|
||||||
|
|
||||||
trace!("settings: {:#?}", settings); |
|
||||||
|
|
||||||
Ok(settings) |
|
||||||
} |
|
||||||
} |
|
@ -1,120 +0,0 @@ |
|||||||
use sprinklers_actors::{state_manager, StateManager}; |
|
||||||
use sprinklers_database::{self as database, DbConn}; |
|
||||||
|
|
||||||
use eyre::{eyre, WrapErr}; |
|
||||||
use sprinklers_core::model::{ProgramRef, Programs, Zones}; |
|
||||||
use tokio::{ |
|
||||||
runtime, |
|
||||||
sync::{mpsc, watch}, |
|
||||||
}; |
|
||||||
use tracing::{trace, warn}; |
|
||||||
|
|
||||||
pub struct StateManagerThread { |
|
||||||
db_conn: DbConn, |
|
||||||
request_rx: mpsc::Receiver<state_manager::Request>, |
|
||||||
zones_tx: watch::Sender<Zones>, |
|
||||||
programs_tx: watch::Sender<Programs>, |
|
||||||
} |
|
||||||
|
|
||||||
struct State { |
|
||||||
zones: Zones, |
|
||||||
programs: Programs, |
|
||||||
} |
|
||||||
|
|
||||||
impl StateManagerThread { |
|
||||||
pub fn start(db_conn: DbConn) -> StateManager { |
|
||||||
let (request_tx, request_rx) = mpsc::channel(8); |
|
||||||
let (zones_tx, zones_rx) = watch::channel(Zones::default()); |
|
||||||
let (programs_tx, programs_rx) = watch::channel(Programs::default()); |
|
||||||
let task = StateManagerThread { |
|
||||||
db_conn, |
|
||||||
request_rx, |
|
||||||
zones_tx, |
|
||||||
programs_tx, |
|
||||||
}; |
|
||||||
let runtime_handle = runtime::Handle::current(); |
|
||||||
std::thread::Builder::new() |
|
||||||
.name("sprinklers_rs::state_manager".into()) |
|
||||||
.spawn(move || task.run(runtime_handle)) |
|
||||||
.expect("could not start state_manager thread"); |
|
||||||
StateManager::new(request_tx, zones_rx, programs_rx) |
|
||||||
} |
|
||||||
|
|
||||||
fn broadcast_zones(&mut self, zones: Zones) { |
|
||||||
if let Err(err) = self.zones_tx.broadcast(zones) { |
|
||||||
warn!("could not broadcast zones: {}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn broadcast_programs(&mut self, programs: Programs) { |
|
||||||
if let Err(err) = self.programs_tx.broadcast(programs) { |
|
||||||
warn!("could not broadcast programs: {}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn handle_request( |
|
||||||
&mut self, |
|
||||||
request: state_manager::Request, |
|
||||||
state: &mut State, |
|
||||||
) -> eyre::Result<()> { |
|
||||||
use state_manager::Request; |
|
||||||
|
|
||||||
match request { |
|
||||||
Request::UpdateProgram { |
|
||||||
id, |
|
||||||
update, |
|
||||||
resp_tx, |
|
||||||
} => { |
|
||||||
// HACK: would really like stable try notation
|
|
||||||
let res = (|| -> state_manager::Result<ProgramRef> { |
|
||||||
let mut trans = self |
|
||||||
.db_conn |
|
||||||
.transaction() |
|
||||||
.wrap_err("failed to start transaction")?; |
|
||||||
database::update_program(&mut trans, id, &update).map_err(|err| { |
|
||||||
if let Some(e) = err.downcast_ref::<database::NoSuchProgram>() { |
|
||||||
state_manager::StateError::NoSuchProgram(e.0) |
|
||||||
} else { |
|
||||||
err.into() |
|
||||||
} |
|
||||||
})?; |
|
||||||
let new_program: ProgramRef = database::query_program_by_id(&trans, id)?.into(); |
|
||||||
state.programs.insert(new_program.id, new_program.clone()); |
|
||||||
trans.commit().wrap_err("could not commit transaction")?; |
|
||||||
self.broadcast_programs(state.programs.clone()); |
|
||||||
Ok(new_program) |
|
||||||
})(); |
|
||||||
resp_tx |
|
||||||
.send(res) |
|
||||||
.map_err(|_| eyre!("could not respond to UpdateProgram"))?; |
|
||||||
} |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
fn load_state(&mut self) -> eyre::Result<State> { |
|
||||||
let zones = database::query_zones(&self.db_conn)?; |
|
||||||
|
|
||||||
for zone in zones.values() { |
|
||||||
trace!(zone = debug(&zone), "read zone"); |
|
||||||
} |
|
||||||
|
|
||||||
let programs = |
|
||||||
database::query_programs(&self.db_conn).wrap_err("could not query programs")?; |
|
||||||
|
|
||||||
Ok(State { zones, programs }) |
|
||||||
} |
|
||||||
|
|
||||||
fn run(mut self, runtime_handle: runtime::Handle) { |
|
||||||
let mut state = self.load_state().expect("could not load initial state"); |
|
||||||
|
|
||||||
self.broadcast_zones(state.zones.clone()); |
|
||||||
self.broadcast_programs(state.programs.clone()); |
|
||||||
|
|
||||||
while let Some(request) = runtime_handle.block_on(self.request_rx.recv()) { |
|
||||||
if let Err(err) = self.handle_request(request, &mut state) { |
|
||||||
warn!("error handling request: {:?}", err); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,33 +0,0 @@ |
|||||||
use sprinklers_core::zone_interface::{MockZoneInterface, ZoneInterface, ZoneNum}; |
|
||||||
|
|
||||||
#[cfg(feature = "sprinklers_linux")] |
|
||||||
use sprinklers_linux::LinuxGpioConfig; |
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)] |
|
||||||
#[serde(tag = "provider")] |
|
||||||
pub enum ZoneInterfaceConfig { |
|
||||||
Mock { |
|
||||||
num_zones: ZoneNum, |
|
||||||
}, |
|
||||||
#[cfg(feature = "sprinklers_linux")] |
|
||||||
LinuxGpio(LinuxGpioConfig), |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for ZoneInterfaceConfig { |
|
||||||
fn default() -> Self { |
|
||||||
ZoneInterfaceConfig::Mock { num_zones: 6 } |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ZoneInterfaceConfig { |
|
||||||
pub fn build(self) -> eyre::Result<Arc<dyn ZoneInterface>> { |
|
||||||
Ok(match self { |
|
||||||
ZoneInterfaceConfig::Mock { num_zones } => Arc::new(MockZoneInterface::new(num_zones)), |
|
||||||
#[cfg(feature = "sprinklers_linux")] |
|
||||||
ZoneInterfaceConfig::LinuxGpio(config) => Arc::new(config.build()?), |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,27 @@ |
|||||||
|
use crate::migrations::{Migrations, SimpleMigration}; |
||||||
|
|
||||||
|
pub fn create_migrations() -> Migrations { |
||||||
|
let mut migs = Migrations::new(); |
||||||
|
migs.add(SimpleMigration::new_box( |
||||||
|
1, |
||||||
|
"CREATE TABLE sections ( |
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
name TEXT NOT NULL, |
||||||
|
interface_id INTEGER NOT NULL |
||||||
|
);", |
||||||
|
"DROP TABLE sections;", |
||||||
|
)); |
||||||
|
migs.add(SimpleMigration::new_box( |
||||||
|
2, |
||||||
|
"INSERT INTO sections (id, name, interface_id) |
||||||
|
VALUES
|
||||||
|
(1, 'Front Yard Middle', 0), |
||||||
|
(2, 'Front Yard Left', 1), |
||||||
|
(3, 'Front Yard Right', 2), |
||||||
|
(4, 'Back Yard Middle', 3), |
||||||
|
(5, 'Back Yard Sauna', 4), |
||||||
|
(6, 'Garden', 5);", |
||||||
|
"DELETE FROM sections;", |
||||||
|
)); |
||||||
|
migs |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
use color_eyre::eyre::Result; |
||||||
|
use rusqlite::Connection as DbConnection; |
||||||
|
use rusqlite::NO_PARAMS; |
||||||
|
use tracing::info; |
||||||
|
use tracing_subscriber::EnvFilter; |
||||||
|
|
||||||
|
mod db; |
||||||
|
mod migrations; |
||||||
|
mod model; |
||||||
|
mod section_interface; |
||||||
|
mod section_runner; |
||||||
|
#[cfg(test)] |
||||||
|
mod trace_listeners; |
||||||
|
|
||||||
|
use model::Section; |
||||||
|
|
||||||
|
fn setup_db() -> Result<DbConnection> { |
||||||
|
// let conn = DbConnection::open_in_memory()?;
|
||||||
|
let mut conn = DbConnection::open("test.db")?; |
||||||
|
|
||||||
|
let migs = db::create_migrations(); |
||||||
|
migs.apply(&mut conn)?; |
||||||
|
|
||||||
|
Ok(conn) |
||||||
|
} |
||||||
|
|
||||||
|
fn query_sections(conn: &DbConnection) -> Result<Vec<Section>> { |
||||||
|
let mut statement = conn.prepare_cached("SELECT id, name, interface_id FROM sections;")?; |
||||||
|
let rows = statement.query_map(NO_PARAMS, Section::from_sql)?; |
||||||
|
Ok(rows.collect::<Result<Vec<_>, _>>()?) |
||||||
|
} |
||||||
|
|
||||||
|
fn main() -> Result<()> { |
||||||
|
tracing_subscriber::fmt() |
||||||
|
.with_ansi(true) |
||||||
|
.with_env_filter( |
||||||
|
EnvFilter::try_from_default_env() |
||||||
|
.unwrap_or_else(|_| EnvFilter::new("info")), |
||||||
|
) |
||||||
|
.init(); |
||||||
|
color_eyre::install()?; |
||||||
|
|
||||||
|
let conn = setup_db()?; |
||||||
|
|
||||||
|
let sections = query_sections(&conn)?; |
||||||
|
for sec in sections { |
||||||
|
info!(section = debug(&sec), "read section"); |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
mod section; |
||||||
|
|
||||||
|
pub use section::{Section, SectionRef}; |
@ -0,0 +1,27 @@ |
|||||||
|
use crate::section_interface::SectionId; |
||||||
|
use rusqlite::{Error as SqlError, Row as SqlRow, ToSql}; |
||||||
|
use std::sync::Arc; |
||||||
|
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub struct Section { |
||||||
|
pub id: u32, |
||||||
|
pub name: String, |
||||||
|
pub interface_id: SectionId, |
||||||
|
} |
||||||
|
|
||||||
|
impl Section { |
||||||
|
pub fn from_sql<'a>(row: &SqlRow<'a>) -> Result<Section, SqlError> { |
||||||
|
Ok(Section { |
||||||
|
id: row.get(0)?, |
||||||
|
name: row.get(1)?, |
||||||
|
interface_id: row.get(2)?, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
#[allow(dead_code)] |
||||||
|
pub fn as_sql(&self) -> Vec<&dyn ToSql> { |
||||||
|
vec![&self.id, &self.name, &self.interface_id] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub type SectionRef = Arc<Section>; |
@ -0,0 +1,65 @@ |
|||||||
|
use std::iter::repeat_with; |
||||||
|
use std::sync::atomic::{AtomicBool, Ordering}; |
||||||
|
use tracing::debug; |
||||||
|
|
||||||
|
pub type SectionId = u32; |
||||||
|
|
||||||
|
pub trait SectionInterface: Send { |
||||||
|
fn num_sections(&self) -> SectionId; |
||||||
|
fn set_section_state(&self, id: SectionId, running: bool); |
||||||
|
fn get_section_state(&self, id: SectionId) -> bool; |
||||||
|
} |
||||||
|
|
||||||
|
pub struct MockSectionInterface { |
||||||
|
states: Vec<AtomicBool>, |
||||||
|
} |
||||||
|
|
||||||
|
impl MockSectionInterface { |
||||||
|
#[allow(dead_code)] |
||||||
|
pub fn new(num_sections: SectionId) -> Self { |
||||||
|
Self { |
||||||
|
states: repeat_with(|| AtomicBool::new(false)) |
||||||
|
.take(num_sections as usize) |
||||||
|
.collect(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl SectionInterface for MockSectionInterface { |
||||||
|
fn num_sections(&self) -> SectionId { |
||||||
|
self.states.len() as SectionId |
||||||
|
} |
||||||
|
fn set_section_state(&self, id: SectionId, running: bool) { |
||||||
|
debug!(id, running, "setting section"); |
||||||
|
self.states[id as usize].store(running, Ordering::SeqCst); |
||||||
|
} |
||||||
|
fn get_section_state(&self, id: SectionId) -> bool { |
||||||
|
self.states[id as usize].load(Ordering::SeqCst) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod test { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_mock_section_interface() { |
||||||
|
let iface = MockSectionInterface::new(6); |
||||||
|
assert_eq!(iface.num_sections(), 6); |
||||||
|
for i in 0..6u32 { |
||||||
|
assert_eq!(iface.get_section_state(i), false); |
||||||
|
} |
||||||
|
for i in 0..6u32 { |
||||||
|
iface.set_section_state(i, true); |
||||||
|
} |
||||||
|
for i in 0..6u32 { |
||||||
|
assert_eq!(iface.get_section_state(i), true); |
||||||
|
} |
||||||
|
for i in 0..6u32 { |
||||||
|
iface.set_section_state(i, false); |
||||||
|
} |
||||||
|
for i in 0..6u32 { |
||||||
|
assert_eq!(iface.get_section_state(i), false); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,490 @@ |
|||||||
|
use crate::model::SectionRef; |
||||||
|
use crate::section_interface::SectionInterface; |
||||||
|
use mpsc::error::SendError; |
||||||
|
use std::{ |
||||||
|
collections::VecDeque, |
||||||
|
mem::swap, |
||||||
|
sync::{ |
||||||
|
atomic::{AtomicI32, Ordering}, |
||||||
|
Arc, |
||||||
|
}, |
||||||
|
time::Duration, |
||||||
|
}; |
||||||
|
use thiserror::Error; |
||||||
|
use tokio::{ |
||||||
|
spawn, |
||||||
|
sync::mpsc, |
||||||
|
time::{delay_for, Instant}, |
||||||
|
}; |
||||||
|
use tracing::{debug, trace, trace_span}; |
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)] |
||||||
|
pub struct RunHandle(i32); |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
struct SectionRunnerInner { |
||||||
|
next_run_id: AtomicI32, |
||||||
|
} |
||||||
|
|
||||||
|
impl SectionRunnerInner { |
||||||
|
fn new() -> Self { |
||||||
|
Self { |
||||||
|
next_run_id: AtomicI32::new(1), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
enum RunnerMsg { |
||||||
|
Quit, |
||||||
|
QueueRun(RunHandle, SectionRef, Duration), |
||||||
|
CancelRun(RunHandle), |
||||||
|
CancelAll, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
enum RunState { |
||||||
|
Waiting, |
||||||
|
Running { start_time: Instant }, |
||||||
|
Finished, |
||||||
|
Cancelled, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
struct SecRun { |
||||||
|
handle: RunHandle, |
||||||
|
section: SectionRef, |
||||||
|
duration: Duration, |
||||||
|
state: RunState, |
||||||
|
} |
||||||
|
|
||||||
|
impl SecRun { |
||||||
|
fn start(&mut self, interface: &dyn SectionInterface) { |
||||||
|
use RunState::*; |
||||||
|
debug!(section_id = self.section.id, "starting running section"); |
||||||
|
interface.set_section_state(self.section.interface_id, true); |
||||||
|
self.state = Running { |
||||||
|
start_time: Instant::now(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
fn is_running(&self) -> bool { |
||||||
|
matches!(self.state, RunState::Running{..}) |
||||||
|
} |
||||||
|
|
||||||
|
fn finish(&mut self, interface: &dyn SectionInterface) { |
||||||
|
if self.is_running() { |
||||||
|
debug!(section_id = self.section.id, "finished running section"); |
||||||
|
interface.set_section_state(self.section.interface_id, false); |
||||||
|
self.state = RunState::Finished; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn cancel(&mut self, interface: &dyn SectionInterface) { |
||||||
|
if self.is_running() { |
||||||
|
debug!(section_id = self.section.id, "cancelling running section"); |
||||||
|
interface.set_section_state(self.section.interface_id, false); |
||||||
|
} |
||||||
|
self.state = RunState::Cancelled; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mod option_future { |
||||||
|
use pin_project::pin_project; |
||||||
|
use std::{ |
||||||
|
future::Future, |
||||||
|
ops::Deref, |
||||||
|
pin::Pin, |
||||||
|
task::{Context, Poll}, |
||||||
|
}; |
||||||
|
|
||||||
|
#[pin_project] |
||||||
|
#[derive(Debug, Clone)] |
||||||
|
#[must_use = "futures do nothing unless you `.await` or poll them"] |
||||||
|
pub struct OptionFuture<F>(#[pin] Option<F>); |
||||||
|
|
||||||
|
impl<F: Future> Future for OptionFuture<F> { |
||||||
|
type Output = Option<F::Output>; |
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { |
||||||
|
match self.project().0.as_pin_mut() { |
||||||
|
Some(x) => x.poll(cx).map(Some), |
||||||
|
None => Poll::Ready(None), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> Deref for OptionFuture<F> { |
||||||
|
type Target = Option<F>; |
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target { |
||||||
|
&self.0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> From<Option<T>> for OptionFuture<T> { |
||||||
|
fn from(option: Option<T>) -> Self { |
||||||
|
OptionFuture(option) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
use option_future::OptionFuture; |
||||||
|
|
||||||
|
async fn runner_task( |
||||||
|
interface: Arc<dyn SectionInterface + Sync>, |
||||||
|
mut msg_recv: mpsc::Receiver<RunnerMsg>, |
||||||
|
) { |
||||||
|
use RunState::*; |
||||||
|
|
||||||
|
let span = trace_span!("runner_task"); |
||||||
|
let _enter = span.enter(); |
||||||
|
|
||||||
|
let mut running = true; |
||||||
|
let mut run_queue: VecDeque<SecRun> = VecDeque::new(); |
||||||
|
let mut delay_future: OptionFuture<_> = None.into(); |
||||||
|
while running { |
||||||
|
if let Some(current_run) = run_queue.front_mut() { |
||||||
|
let done = match current_run.state { |
||||||
|
Waiting => { |
||||||
|
current_run.start(&*interface); |
||||||
|
delay_future = Some(delay_for(current_run.duration)).into(); |
||||||
|
false |
||||||
|
} |
||||||
|
Running { start_time } => { |
||||||
|
if start_time.elapsed() >= current_run.duration { |
||||||
|
current_run.finish(&*interface); |
||||||
|
delay_future = None.into(); |
||||||
|
true |
||||||
|
} else { |
||||||
|
false |
||||||
|
} |
||||||
|
} |
||||||
|
Cancelled | Finished => true, |
||||||
|
}; |
||||||
|
|
||||||
|
if done { |
||||||
|
run_queue.pop_front(); |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let mut handle_msg = |msg: Option<RunnerMsg>| { |
||||||
|
let msg = msg.expect("SectionRunner channel closed"); |
||||||
|
use RunnerMsg::*; |
||||||
|
trace!(msg = debug(&msg), "runner_task recv"); |
||||||
|
match msg { |
||||||
|
Quit => running = false, |
||||||
|
QueueRun(handle, section, duration) => { |
||||||
|
run_queue.push_back(SecRun { |
||||||
|
handle, |
||||||
|
section, |
||||||
|
duration, |
||||||
|
state: Waiting, |
||||||
|
}); |
||||||
|
} |
||||||
|
CancelRun(handle) => { |
||||||
|
for run in &mut run_queue { |
||||||
|
if run.handle != handle { |
||||||
|
continue; |
||||||
|
} |
||||||
|
trace!(handle = handle.0, "cancelling run by handle"); |
||||||
|
run.cancel(&*interface); |
||||||
|
} |
||||||
|
} |
||||||
|
CancelAll => { |
||||||
|
let mut old_runs = VecDeque::new(); |
||||||
|
swap(&mut old_runs, &mut run_queue); |
||||||
|
trace!(count = old_runs.len(), "cancelling all runs"); |
||||||
|
for mut run in old_runs { |
||||||
|
run.cancel(&*interface); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
let delay_done = || { |
||||||
|
trace!("delay done"); |
||||||
|
}; |
||||||
|
tokio::select! { |
||||||
|
msg = msg_recv.recv() => handle_msg(msg), |
||||||
|
_ = &mut delay_future, if delay_future.is_some() => delay_done() |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)] |
||||||
|
#[error("the SectionRunner channel is closed")] |
||||||
|
pub struct ChannelClosed; |
||||||
|
|
||||||
|
pub type Result<T, E = ChannelClosed> = std::result::Result<T, E>; |
||||||
|
|
||||||
|
impl<T> From<SendError<T>> for ChannelClosed { |
||||||
|
fn from(_: SendError<T>) -> Self { |
||||||
|
Self |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct SectionRunner { |
||||||
|
inner: Arc<SectionRunnerInner>, |
||||||
|
msg_send: mpsc::Sender<RunnerMsg>, |
||||||
|
} |
||||||
|
|
||||||
|
#[allow(dead_code)] |
||||||
|
impl SectionRunner { |
||||||
|
pub fn new(interface: Arc<dyn SectionInterface + Sync>) -> Self { |
||||||
|
let (msg_send, msg_recv) = mpsc::channel(8); |
||||||
|
spawn(runner_task(interface, msg_recv)); |
||||||
|
Self { |
||||||
|
inner: Arc::new(SectionRunnerInner::new()), |
||||||
|
msg_send, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn quit(&mut self) -> Result<()> { |
||||||
|
self.msg_send.send(RunnerMsg::Quit).await?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn queue_run( |
||||||
|
&mut self, |
||||||
|
section: SectionRef, |
||||||
|
duration: Duration, |
||||||
|
) -> Result<RunHandle> { |
||||||
|
let run_id = self.inner.next_run_id.fetch_add(1, Ordering::Relaxed); |
||||||
|
let handle = RunHandle(run_id); |
||||||
|
self.msg_send |
||||||
|
.send(RunnerMsg::QueueRun(handle.clone(), section, duration)) |
||||||
|
.await?; |
||||||
|
Ok(handle) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn cancel_run(&mut self, handle: RunHandle) -> Result<()> { |
||||||
|
self.msg_send.send(RunnerMsg::CancelRun(handle)).await?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn cancel_all(&mut self) -> Result<()> { |
||||||
|
self.msg_send.send(RunnerMsg::CancelAll).await?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn pause(&mut self) -> Result<()> { |
||||||
|
todo!() |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn unpause(&mut self) -> Result<()> { |
||||||
|
todo!() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod test { |
||||||
|
use super::*; |
||||||
|
use crate::section_interface::MockSectionInterface; |
||||||
|
use crate::{ |
||||||
|
model::Section, |
||||||
|
trace_listeners::{EventListener, Filters, SpanFilters, SpanListener}, |
||||||
|
}; |
||||||
|
use tokio::time::{pause, resume}; |
||||||
|
use tracing_subscriber::prelude::*; |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_quit() { |
||||||
|
let quit_msg = EventListener::new( |
||||||
|
Filters::new() |
||||||
|
.target("sprinklers_rs::section_runner") |
||||||
|
.message("runner_task recv") |
||||||
|
.field_value("msg", "Quit"), |
||||||
|
); |
||||||
|
let task_span = SpanListener::new( |
||||||
|
SpanFilters::new() |
||||||
|
.target("sprinklers_rs::section_runner") |
||||||
|
.name("runner_task"), |
||||||
|
); |
||||||
|
let subscriber = tracing_subscriber::registry() |
||||||
|
.with(quit_msg.clone()) |
||||||
|
.with(task_span.clone()); |
||||||
|
let _sub = tracing::subscriber::set_default(subscriber); |
||||||
|
|
||||||
|
let interface = MockSectionInterface::new(6); |
||||||
|
let mut runner = SectionRunner::new(Arc::new(interface)); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
runner.quit().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
|
||||||
|
assert_eq!(quit_msg.get_count(), 1); |
||||||
|
assert_eq!(task_span.get_exit_count(), 1); |
||||||
|
} |
||||||
|
|
||||||
|
fn make_sections_and_interface() -> (Vec<SectionRef>, Arc<MockSectionInterface>) { |
||||||
|
let interface = Arc::new(MockSectionInterface::new(2)); |
||||||
|
let sections: Vec<SectionRef> = vec![ |
||||||
|
Arc::new(Section { |
||||||
|
id: 1, |
||||||
|
name: "Section 1".into(), |
||||||
|
interface_id: 0, |
||||||
|
}), |
||||||
|
Arc::new(Section { |
||||||
|
id: 2, |
||||||
|
name: "Section 2".into(), |
||||||
|
interface_id: 1, |
||||||
|
}), |
||||||
|
]; |
||||||
|
(sections, interface) |
||||||
|
} |
||||||
|
|
||||||
|
fn assert_section_states(interface: &MockSectionInterface, states: &[bool]) { |
||||||
|
for (id, state) in states.iter().enumerate() { |
||||||
|
assert_eq!( |
||||||
|
interface.get_section_state(id as u32), |
||||||
|
*state, |
||||||
|
"section interface id {} did not match", |
||||||
|
id |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async fn advance(dur: Duration) { |
||||||
|
// HACK: advance should really be enough, but we need another yield_now
|
||||||
|
tokio::time::advance(dur).await; |
||||||
|
tokio::task::yield_now().await; |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_queue() { |
||||||
|
let (sections, interface) = make_sections_and_interface(); |
||||||
|
let mut runner = SectionRunner::new(interface.clone()); |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
// Queue single section, make sure it runs
|
||||||
|
runner |
||||||
|
.queue_run(sections[0].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
tokio::task::yield_now().await; |
||||||
|
|
||||||
|
pause(); |
||||||
|
advance(Duration::from_secs(1)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[true, false]); |
||||||
|
|
||||||
|
advance(Duration::from_secs(10)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
// Queue two sections, make sure they run one at a time
|
||||||
|
runner |
||||||
|
.queue_run(sections[1].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
runner |
||||||
|
.queue_run(sections[0].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
advance(Duration::from_secs(1)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, true]); |
||||||
|
|
||||||
|
advance(Duration::from_secs(10)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[true, false]); |
||||||
|
|
||||||
|
advance(Duration::from_secs(10)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
resume(); |
||||||
|
|
||||||
|
runner.quit().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_cancel_run() { |
||||||
|
let (sections, interface) = make_sections_and_interface(); |
||||||
|
let mut runner = SectionRunner::new(interface.clone()); |
||||||
|
|
||||||
|
let run1 = runner |
||||||
|
.queue_run(sections[1].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let _run2 = runner |
||||||
|
.queue_run(sections[0].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let run3 = runner |
||||||
|
.queue_run(sections[1].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
pause(); |
||||||
|
|
||||||
|
advance(Duration::from_secs(1)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, true]); |
||||||
|
|
||||||
|
runner.cancel_run(run1).await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[true, false]); |
||||||
|
|
||||||
|
runner.cancel_run(run3).await.unwrap(); |
||||||
|
advance(Duration::from_secs(10)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
resume(); |
||||||
|
|
||||||
|
runner.quit().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_cancel_all() { |
||||||
|
let (sections, interface) = make_sections_and_interface(); |
||||||
|
let mut runner = SectionRunner::new(interface.clone()); |
||||||
|
|
||||||
|
runner |
||||||
|
.queue_run(sections[1].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
runner |
||||||
|
.queue_run(sections[0].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
runner |
||||||
|
.queue_run(sections[1].clone(), Duration::from_secs(10)) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
pause(); |
||||||
|
|
||||||
|
advance(Duration::from_secs(1)).await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, true]); |
||||||
|
|
||||||
|
runner.cancel_all().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
runner.cancel_all().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
|
||||||
|
assert_section_states(&interface, &[false, false]); |
||||||
|
|
||||||
|
resume(); |
||||||
|
|
||||||
|
runner.quit().await.unwrap(); |
||||||
|
tokio::task::yield_now().await; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue