Move zone handling to state manager
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			And zone publishing to update listener
This commit is contained in:
		
							parent
							
								
									47f563c0a9
								
							
						
					
					
						commit
						4eb2043ad7
					
				| @ -316,6 +316,24 @@ impl Handler<CancelProgram> for ProgramRunnerActor { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl StreamHandler<Zones> for ProgramRunnerActor { | ||||
|     fn handle(&mut self, item: Zones, ctx: &mut Self::Context) { | ||||
|         ctx.notify(UpdateZones(item)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "()")] | ||||
| struct ListenZones(watch::Receiver<Zones>); | ||||
| 
 | ||||
| impl Handler<ListenZones> for ProgramRunnerActor { | ||||
|     type Result = (); | ||||
| 
 | ||||
|     fn handle(&mut self, msg: ListenZones, ctx: &mut Self::Context) -> Self::Result { | ||||
|         ctx.add_stream(msg.0); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl StreamHandler<Programs> for ProgramRunnerActor { | ||||
|     fn handle(&mut self, item: Programs, ctx: &mut Self::Context) { | ||||
|         ctx.notify(UpdatePrograms(item)) | ||||
| @ -498,6 +516,11 @@ impl ProgramRunner { | ||||
|         Ok(event_recv) | ||||
|     } | ||||
| 
 | ||||
|     pub fn listen_zones(&mut self, zones_watch: watch::Receiver<Zones>) { | ||||
|         // TODO: should this adopt a similar pattern to update_listener?
 | ||||
|         self.addr.do_send(ListenZones(zones_watch)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn listen_programs(&mut self, programs_watch: watch::Receiver<Programs>) { | ||||
|         self.addr.do_send(ListenPrograms(programs_watch)) | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| use sprinklers_core::model::{ProgramId, ProgramRef, ProgramUpdateData, Programs}; | ||||
| use sprinklers_core::model::{ProgramId, ProgramRef, ProgramUpdateData, Programs, Zones}; | ||||
| use thiserror::Error; | ||||
| use tokio::sync::{mpsc, oneshot, watch}; | ||||
| 
 | ||||
| @ -14,6 +14,7 @@ pub enum Request { | ||||
| #[derive(Clone)] | ||||
| pub struct StateManager { | ||||
|     request_tx: mpsc::Sender<Request>, | ||||
|     zones_watch: watch::Receiver<Zones>, | ||||
|     programs_watch: watch::Receiver<Programs>, | ||||
| } | ||||
| 
 | ||||
| @ -34,10 +35,12 @@ 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, | ||||
|         } | ||||
|     } | ||||
| @ -59,6 +62,10 @@ impl StateManager { | ||||
|         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() | ||||
|     } | ||||
|  | ||||
| @ -75,15 +75,42 @@ impl MqttInterface { | ||||
| 
 | ||||
|     pub async fn publish_zones(&mut self, zones: &Zones) -> eyre::Result<()> { | ||||
|         let zone_ids: Vec<_> = zones.keys().cloned().collect(); | ||||
|         self.publish_data(self.topics.zones(), &zone_ids) | ||||
|             .await | ||||
|             .wrap_err("failed to publish zone ids")?; | ||||
|         self.publish_zone_ids(&zone_ids).await?; | ||||
|         for zone in zones.values() { | ||||
|             self.publish_zone(zone).await?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     // TODO: figure out how to share logic with publish_programs_diff and publish_zones
 | ||||
|     pub async fn publish_zones_diff( | ||||
|         &mut self, | ||||
|         old_zones: &Zones, | ||||
|         zones: &Zones, | ||||
|     ) -> eyre::Result<()> { | ||||
|         for (id, zone) in zones { | ||||
|             let publish = match old_zones.get(id) { | ||||
|                 Some(old_zone) => !Arc::ptr_eq(old_zone, zone), | ||||
|                 None => { | ||||
|                     let zone_ids: Vec<_> = zones.keys().cloned().collect(); | ||||
|                     self.publish_zone_ids(&zone_ids).await?; | ||||
|                     true | ||||
|                 } | ||||
|             }; | ||||
|             if publish { | ||||
|                 self.publish_zone(zone).await?; | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn publish_zone_ids(&mut self, zone_ids: &[ZoneId]) -> eyre::Result<()> { | ||||
|         self.publish_data(self.topics.zones(), &zone_ids) | ||||
|             .await | ||||
|             .wrap_err("failed to publish zone ids")?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub async fn publish_zone(&mut self, zone: &Zone) -> eyre::Result<()> { | ||||
|         self.publish_data(self.topics.zone_data(zone.id), zone) | ||||
|             .await | ||||
|  | ||||
| @ -5,12 +5,13 @@ 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: Zones, | ||||
|     pub zones: watch::Receiver<Zones>, | ||||
|     pub zone_runner: ZoneRunner, | ||||
|     pub program_runner: ProgramRunner, | ||||
|     pub state_manager: StateManager, | ||||
|  | ||||
| @ -41,7 +41,7 @@ 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); | ||||
|         let zone = self.zone_id.get_zone(&*ctx.zones.borrow()); | ||||
|         let duration = self.duration; | ||||
|         Box::pin(async move { | ||||
|             let zone = zone?; | ||||
| @ -76,7 +76,7 @@ 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); | ||||
|         let zone = self.zone_id.get_zone(&*ctx.zones.borrow()); | ||||
|         Box::pin(async move { | ||||
|             let zone = zone?; | ||||
|             let cancelled = zone_runner | ||||
|  | ||||
| @ -6,12 +6,13 @@ use sprinklers_actors::{ | ||||
| 
 | ||||
| use actix::{fut::wrap_future, Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler}; | ||||
| use futures_util::TryFutureExt; | ||||
| use sprinklers_core::model::Programs; | ||||
| 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>, | ||||
| } | ||||
| 
 | ||||
| @ -19,6 +20,7 @@ impl UpdateListenerActor { | ||||
|     fn new(mqtt_interface: MqttInterface) -> Self { | ||||
|         Self { | ||||
|             mqtt_interface, | ||||
|             old_zones: None, | ||||
|             old_programs: None, | ||||
|         } | ||||
|     } | ||||
| @ -36,6 +38,42 @@ impl Actor for UpdateListenerActor { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
|             mqtt_interface.publish_zones(&zones).await?; | ||||
|             for zone_id in zones.keys() { | ||||
|                 mqtt_interface.publish_zone_state(*zone_id, false).await?; | ||||
|             } | ||||
| 
 | ||||
|             match old_zones { | ||||
|                 None => { | ||||
|                     mqtt_interface.publish_zones(&zones).await?; | ||||
| 
 | ||||
|                     // 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.publish_zone_state(*zone_id, false).await?; | ||||
|                     } | ||||
|                 } | ||||
|                 Some(old_zones) => { | ||||
|                     mqtt_interface | ||||
|                         .publish_zones_diff(&old_zones, &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 { | ||||
| @ -196,6 +234,12 @@ where | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
| @ -237,6 +281,10 @@ impl UpdateListener { | ||||
|         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); | ||||
|     } | ||||
|  | ||||
| @ -12,7 +12,7 @@ use sprinklers_mqtt as mqtt; | ||||
| 
 | ||||
| use eyre::{Result, WrapErr}; | ||||
| use settings::Settings; | ||||
| use tracing::{debug, info}; | ||||
| use tracing::info; | ||||
| use tracing_subscriber::EnvFilter; | ||||
| 
 | ||||
| #[actix_rt::main] | ||||
| @ -31,11 +31,6 @@ async fn main() -> Result<()> { | ||||
| 
 | ||||
|     let db_conn = database::setup_db()?; | ||||
| 
 | ||||
|     let zones = database::query_zones(&db_conn)?; | ||||
|     for zone in zones.values() { | ||||
|         debug!(zone = debug(&zone), "read zone"); | ||||
|     } | ||||
| 
 | ||||
|     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()); | ||||
| @ -45,29 +40,24 @@ async fn main() -> Result<()> { | ||||
|     let mqtt_options = settings.mqtt; | ||||
|     // TODO: have ability to update zones / other data
 | ||||
|     let request_context = mqtt::RequestContext { | ||||
|         zones: zones.clone(), | ||||
|         zones: state_manager.get_zones(), | ||||
|         zone_runner: zone_runner.clone(), | ||||
|         program_runner: program_runner.clone(), | ||||
|         state_manager: state_manager.clone(), | ||||
|     }; | ||||
|     let mut mqtt_interface = mqtt::MqttInterfaceTask::start(mqtt_options, request_context); | ||||
|     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()); | ||||
| 
 | ||||
|     program_runner.update_zones(zones.clone()).await?; | ||||
|     // TODO: update listener should probably do this
 | ||||
|     mqtt_interface.publish_zones(&zones).await?; | ||||
|     for zone_id in zones.keys() { | ||||
|         mqtt_interface.publish_zone_state(*zone_id, false).await?; | ||||
|     } | ||||
| 
 | ||||
|     info!("sprinklers_rs initialized"); | ||||
| 
 | ||||
|     tokio::signal::ctrl_c().await?; | ||||
|  | ||||
| @ -2,30 +2,34 @@ use sprinklers_actors::{state_manager, StateManager}; | ||||
| use sprinklers_database::{self as database, DbConn}; | ||||
| 
 | ||||
| use eyre::{eyre, WrapErr}; | ||||
| use sprinklers_core::model::{ProgramRef, Programs}; | ||||
| use sprinklers_core::model::{ProgramRef, Programs, Zones}; | ||||
| use tokio::{ | ||||
|     runtime, | ||||
|     sync::{mpsc, watch}, | ||||
| }; | ||||
| use tracing::warn; | ||||
| 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(); | ||||
| @ -33,7 +37,13 @@ impl StateManagerThread { | ||||
|             .name("sprinklers_rs::state_manager".into()) | ||||
|             .spawn(move || task.run(runtime_handle)) | ||||
|             .expect("could not start state_manager thread"); | ||||
|         StateManager::new(request_tx, programs_rx) | ||||
|         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) { | ||||
| @ -83,15 +93,22 @@ impl StateManagerThread { | ||||
|     } | ||||
| 
 | ||||
|     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 { 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()) { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user