parent
9bc75b9627
commit
b742f0f8d6
127 changed files with 38 additions and 5722 deletions
188
src/app_state.rs
Normal file
188
src/app_state.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use actix::{Actor, Addr, Context, Handler, Message, Recipient};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::{convert_db_list, Controller, Relay};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
|
||||
use futures::executor::block_on;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::handlers::v1::ws::relays::{RelaysWs, SendRelays};
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct DisconnectController {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct ConnectController {
|
||||
pub address: Recipient<ControllerWsAction>,
|
||||
pub controller: Controller,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct UpdateRelayStates {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
pub relay_states: RelayStates,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<Vec<Relay>, EmgauwaError>")]
|
||||
pub struct GetRelays {}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct Action {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
pub action: ControllerWsAction,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ConnectRelayClient {
|
||||
pub addr: Addr<RelaysWs>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub pool: Pool<Sqlite>,
|
||||
pub connected_controllers: HashMap<EmgauwaUid, (Controller, Recipient<ControllerWsAction>)>,
|
||||
pub connected_relay_clients: Vec<Addr<RelaysWs>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(pool: Pool<Sqlite>) -> AppState {
|
||||
AppState {
|
||||
pool,
|
||||
connected_controllers: HashMap::new(),
|
||||
connected_relay_clients: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_relays(&self) -> Result<Vec<Relay>, EmgauwaError> {
|
||||
let mut pool_conn = block_on(self.pool.acquire())?;
|
||||
let db_controllers = block_on(DbController::get_all(&mut pool_conn))?;
|
||||
let mut controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
|
||||
|
||||
self.connected_controllers
|
||||
.iter()
|
||||
.for_each(|(uid, (connected_controller, _))| {
|
||||
if let Some(c) = controllers.iter_mut().find(|c| c.c.uid == *uid) {
|
||||
c.apply_relay_states(&connected_controller.get_relay_states());
|
||||
}
|
||||
});
|
||||
|
||||
let mut relays: Vec<Relay> = Vec::new();
|
||||
controllers.iter().for_each(|c| {
|
||||
relays.extend(c.relays.clone());
|
||||
});
|
||||
|
||||
Ok(relays)
|
||||
}
|
||||
|
||||
fn notify_relay_clients(&mut self) {
|
||||
self.connected_relay_clients.retain(|addr| addr.connected());
|
||||
|
||||
match self.get_relays() {
|
||||
Ok(relays) => match serde_json::to_string(&relays) {
|
||||
Ok(json) => {
|
||||
self.connected_relay_clients.iter_mut().for_each(|addr| {
|
||||
let relays_json = json.clone();
|
||||
addr.do_send(SendRelays { relays_json });
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to serialize relays: {:?}", err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Failed to get relays: {:?}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AppState {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<DisconnectController> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: DisconnectController, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut pool_conn = block_on(self.pool.acquire())?;
|
||||
|
||||
if let Some((controller, address)) = self.connected_controllers.remove(&msg.controller_uid)
|
||||
{
|
||||
if let Err(err) = block_on(controller.c.update_active(&mut pool_conn, false)) {
|
||||
log::error!(
|
||||
"Failed to mark controller {} as inactive: {:?}",
|
||||
controller.c.uid,
|
||||
err
|
||||
);
|
||||
}
|
||||
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
|
||||
//block_on(address.send(ControllerWsAction::Disconnect))??;
|
||||
address.do_send(ControllerWsAction::Disconnect);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ConnectController> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: ConnectController, _ctx: &mut Self::Context) -> Self::Result {
|
||||
log::debug!("Connecting controller: {}", msg.controller.c.uid);
|
||||
self.connected_controllers
|
||||
.insert(msg.controller.c.uid.clone(), (msg.controller, msg.address));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<UpdateRelayStates> for AppState {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateRelayStates, _ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some((controller, _)) = self.connected_controllers.get_mut(&msg.controller_uid) {
|
||||
controller.apply_relay_states(&msg.relay_states);
|
||||
}
|
||||
self.notify_relay_clients();
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<GetRelays> for AppState {
|
||||
type Result = Result<Vec<Relay>, EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, _msg: GetRelays, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.get_relays()
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<Action> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: Action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
log::debug!("Forwarding action: {:?}", msg.action);
|
||||
if let Some((_, address)) = self.connected_controllers.get(&msg.controller_uid) {
|
||||
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
|
||||
//block_on(address.send(msg.action))?
|
||||
address.do_send(msg.action);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EmgauwaError::Connection(msg.controller_uid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ConnectRelayClient> for AppState {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ConnectRelayClient, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.connected_relay_clients.push(msg.addr);
|
||||
}
|
||||
}
|
37
src/handlers/mod.rs
Normal file
37
src/handlers/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use actix_web::{error, Error, HttpRequest, HttpResponse};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
pub mod v1;
|
||||
|
||||
enum EmgauwaJsonPayLoadError {
|
||||
Error(error::JsonPayloadError),
|
||||
}
|
||||
|
||||
impl Serialize for EmgauwaJsonPayLoadError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_struct("error", 3)?;
|
||||
s.serialize_field("type", "json-payload-error")?;
|
||||
s.serialize_field("code", &400)?;
|
||||
s.serialize_field(
|
||||
"description",
|
||||
&match self {
|
||||
EmgauwaJsonPayLoadError::Error(err) => format!("{}", err),
|
||||
},
|
||||
)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_error_handler(err: error::JsonPayloadError, _: &HttpRequest) -> Error {
|
||||
error::InternalError::from_response(
|
||||
"",
|
||||
HttpResponse::BadRequest()
|
||||
.content_type("application/json")
|
||||
.json(EmgauwaJsonPayLoadError::Error(err)),
|
||||
)
|
||||
.into()
|
||||
}
|
92
src/handlers/v1/controllers.rs
Normal file
92
src/handlers/v1/controllers.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, put, web, HttpResponse};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, Controller, FromDbModel};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RequestControllerUpdate};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/controllers")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_controllers = DbController::get_all(&mut pool_conn).await?;
|
||||
|
||||
let controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(controllers))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
|
||||
Ok(HttpResponse::Ok().json(return_controller))
|
||||
}
|
||||
|
||||
#[put("/controllers/{controller_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestControllerUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let controller = controller
|
||||
.update(&mut pool_conn, data.name.as_str(), controller.relay_count)
|
||||
.await?;
|
||||
|
||||
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid.clone(),
|
||||
action: ControllerWsAction::Controller(return_controller.clone()),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().json(return_controller))
|
||||
}
|
||||
|
||||
#[delete("/controllers/{controller_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
app_state
|
||||
.send(app_state::DisconnectController {
|
||||
controller_uid: uid.clone(),
|
||||
})
|
||||
.await??;
|
||||
|
||||
DbController::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("controller got deleted"))
|
||||
}
|
161
src/handlers/v1/macros.rs
Normal file
161
src/handlers/v1/macros.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::DbMacro;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Macro, MacroAction, Relay};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, EmgauwaUid, RequestMacroCreate, RequestMacroExecute, RequestMacroUpdate,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/macros")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_macros = DbMacro::get_all(&mut pool_conn).await?;
|
||||
let macros: Vec<Macro> = convert_db_list(&mut pool_conn, db_macros)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(macros))
|
||||
}
|
||||
|
||||
#[get("/macros/{macro_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
|
||||
Ok(HttpResponse::Ok().json(return_macro))
|
||||
}
|
||||
|
||||
#[post("/macros")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestMacroCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_macro = DbMacro::create(&mut pool_conn, EmgauwaUid::default(), &data.name).await?;
|
||||
|
||||
new_macro
|
||||
.set_actions(&mut pool_conn, data.actions.as_slice())
|
||||
.await?;
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, new_macro)?;
|
||||
Ok(HttpResponse::Created().json(return_macro))
|
||||
}
|
||||
|
||||
#[put("/macros/{macro_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestMacroUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
if let Some(name) = &data.name {
|
||||
db_macro.update(&mut pool_conn, name).await?;
|
||||
}
|
||||
|
||||
if let Some(actions) = &data.actions {
|
||||
db_macro
|
||||
.set_actions(&mut pool_conn, actions.as_slice())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
|
||||
Ok(HttpResponse::Ok().json(return_macro))
|
||||
}
|
||||
|
||||
#[delete("/macros/{macro_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
DbMacro::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("macro got deleted"))
|
||||
}
|
||||
|
||||
#[put("/macros/{macro_id}/execute")]
|
||||
pub async fn execute(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
query: web::Query<RequestMacroExecute>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let actions_db = match query.weekday {
|
||||
None => db_macro.get_actions(&mut pool_conn).await?,
|
||||
Some(weekday) => {
|
||||
db_macro
|
||||
.get_actions_weekday(&mut pool_conn, weekday)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let mut actions: Vec<MacroAction> = convert_db_list(&mut pool_conn, actions_db)?;
|
||||
|
||||
for action in &actions {
|
||||
action.execute(&mut pool_conn).await?;
|
||||
}
|
||||
|
||||
let affected_controller_uids: Vec<EmgauwaUid> = actions
|
||||
.iter()
|
||||
.map(|action| action.relay.controller_id.clone())
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
for controller_uid in affected_controller_uids {
|
||||
let mut affected_relays: Vec<Relay> = Vec::new();
|
||||
let mut affected_relay_ids: Vec<i64> = Vec::new();
|
||||
|
||||
for action in actions.iter_mut() {
|
||||
if affected_relay_ids.contains(&action.relay.r.id) {
|
||||
continue;
|
||||
}
|
||||
action.relay.reload(&mut pool_conn)?;
|
||||
affected_relays.push(action.relay.clone());
|
||||
affected_relay_ids.push(action.relay.r.id);
|
||||
}
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid,
|
||||
action: ControllerWsAction::Relays(affected_relays.clone()),
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().finish()) // TODO add a message?
|
||||
}
|
6
src/handlers/v1/mod.rs
Normal file
6
src/handlers/v1/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod controllers;
|
||||
pub mod macros;
|
||||
pub mod relays;
|
||||
pub mod schedules;
|
||||
pub mod tags;
|
||||
pub mod ws;
|
185
src/handlers/v1/relays.rs
Normal file
185
src/handlers/v1/relays.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbTag};
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Relay};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, EmgauwaUid, RequestRelayPulse, RequestRelayUpdate,
|
||||
};
|
||||
use emgauwa_common::utils;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/relays")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_relays = DbRelay::get_all(&mut pool_conn).await?;
|
||||
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/relays/tag/{tag}")]
|
||||
pub async fn tagged(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag,) = path.into_inner();
|
||||
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_relays = DbRelay::get_by_tag(&mut pool_conn, &tag_db).await?;
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}/relays")]
|
||||
pub async fn index_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_relays = controller.get_relays(&mut pool_conn).await?;
|
||||
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}/relays/{relay_num}")]
|
||||
pub async fn show_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
|
||||
Ok(HttpResponse::Ok().json(return_relay))
|
||||
}
|
||||
|
||||
#[put("/controllers/{controller_id}/relays/{relay_num}")]
|
||||
pub async fn update_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
data: web::Json<RequestRelayUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let mut relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
if let Some(name) = &data.name {
|
||||
relay = relay.update(&mut pool_conn, name.as_str()).await?;
|
||||
}
|
||||
|
||||
if let Some(schedule_uids) = &data.schedules {
|
||||
if schedule_uids.len() == 7 {
|
||||
let mut schedules = Vec::new();
|
||||
for s_uid in schedule_uids {
|
||||
schedules.push(s_uid.get_schedule(&mut pool_conn).await?);
|
||||
}
|
||||
|
||||
DbJunctionRelaySchedule::set_schedules(
|
||||
&mut pool_conn,
|
||||
&relay,
|
||||
schedules.iter().collect(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(s_uid) = &data.active_schedule {
|
||||
let schedule = s_uid.get_schedule(&mut pool_conn).await?;
|
||||
DbJunctionRelaySchedule::set_schedule(
|
||||
&mut pool_conn,
|
||||
&relay,
|
||||
&schedule,
|
||||
utils::get_weekday(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
relay.set_tags(&mut pool_conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
let relay = relay.reload(&mut pool_conn).await?;
|
||||
|
||||
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid,
|
||||
action: ControllerWsAction::Relays(vec![return_relay.clone()]),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().json(return_relay))
|
||||
}
|
||||
|
||||
#[post("/controllers/{controller_id}/relays/{relay_num}/pulse")]
|
||||
pub async fn pulse(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
data: web::Json<RequestRelayPulse>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let duration = data.duration.filter(|&d| d > 0);
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid,
|
||||
action: ControllerWsAction::RelayPulse((relay.number, duration)),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().finish()) // TODO add a message?
|
||||
}
|
196
src/handlers/v1/schedules.rs
Normal file
196
src/handlers/v1/schedules.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbSchedule, DbTag};
|
||||
use emgauwa_common::errors::{ApiError, DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Schedule};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, RequestScheduleCreate, RequestScheduleUpdate, ScheduleUid,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/schedules")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_schedules = DbSchedule::get_all(&mut pool_conn).await?;
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(schedules))
|
||||
}
|
||||
|
||||
#[get("/schedules/tag/{tag}")]
|
||||
pub async fn tagged(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag,) = path.into_inner();
|
||||
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_schedules = DbSchedule::get_by_tag(&mut pool_conn, &tag_db).await?;
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(schedules))
|
||||
}
|
||||
|
||||
#[get("/schedules/{schedule_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
|
||||
Ok(HttpResponse::Ok().json(return_schedule))
|
||||
}
|
||||
|
||||
#[post("/schedules")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestScheduleCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_schedule = DbSchedule::create(
|
||||
&mut pool_conn,
|
||||
ScheduleUid::default(),
|
||||
&data.name,
|
||||
&data.periods,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
new_schedule
|
||||
.set_tags(&mut pool_conn, tags.as_slice())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, new_schedule)?;
|
||||
Ok(HttpResponse::Created().json(return_schedule))
|
||||
}
|
||||
|
||||
async fn add_list_single(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
request_schedule: &RequestScheduleCreate,
|
||||
) -> Result<DbSchedule, DatabaseError> {
|
||||
let new_schedule = DbSchedule::create(
|
||||
conn,
|
||||
ScheduleUid::default(),
|
||||
&request_schedule.name,
|
||||
&request_schedule.periods,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(tags) = &request_schedule.tags {
|
||||
new_schedule.set_tags(conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
Ok(new_schedule)
|
||||
}
|
||||
|
||||
#[post("/schedules/list")]
|
||||
pub async fn add_list(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<Vec<RequestScheduleCreate>>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let mut db_schedules: Vec<DbSchedule> = Vec::new();
|
||||
for s in data.iter() {
|
||||
let new_s = futures::executor::block_on(add_list_single(&mut pool_conn, s))?;
|
||||
db_schedules.push(new_s);
|
||||
}
|
||||
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
Ok(HttpResponse::Created().json(schedules))
|
||||
}
|
||||
|
||||
#[put("/schedules/{schedule_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestScheduleUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let name = match &data.name {
|
||||
None => schedule.name.as_str(),
|
||||
Some(name) => name.as_str(),
|
||||
};
|
||||
|
||||
let periods = match &data.periods {
|
||||
None => &schedule.periods,
|
||||
Some(period) => period,
|
||||
};
|
||||
|
||||
let schedule = schedule.update(&mut pool_conn, name, periods).await?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
schedule.set_tags(&mut pool_conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
let controller_ids: Vec<i64> = DbJunctionRelaySchedule::get_relays(&mut pool_conn, &schedule)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| r.controller_id)
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
for controller_id in controller_ids {
|
||||
let controller = DbController::get(&mut pool_conn, controller_id)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: controller.uid,
|
||||
action: ControllerWsAction::Schedules(vec![schedule.clone()]),
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
|
||||
Ok(HttpResponse::Ok().json(return_schedule))
|
||||
}
|
||||
|
||||
#[delete("/schedules/{schedule_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
match uid {
|
||||
ScheduleUid::Off => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
|
||||
ScheduleUid::On => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
|
||||
ScheduleUid::Any(_) => {
|
||||
DbSchedule::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("schedule got deleted"))
|
||||
}
|
||||
}
|
||||
}
|
61
src/handlers/v1/tags.rs
Normal file
61
src/handlers/v1/tags.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use actix_web::{delete, get, post, web, HttpResponse};
|
||||
use emgauwa_common::db::DbTag;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{FromDbModel, Tag};
|
||||
use emgauwa_common::types::RequestTagCreate;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
#[get("/tags")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_tags = DbTag::get_all(&mut pool_conn).await?;
|
||||
|
||||
let tags: Vec<String> = db_tags.iter().map(|t| t.tag.clone()).collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(tags))
|
||||
}
|
||||
|
||||
#[get("/tags/{tag_name}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag_name,) = path.into_inner();
|
||||
|
||||
let tag = DbTag::get_by_tag(&mut pool_conn, &tag_name)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_tag = Tag::from_db_model(&mut pool_conn, tag)?;
|
||||
Ok(HttpResponse::Ok().json(return_tag))
|
||||
}
|
||||
|
||||
#[delete("/tags/{tag_name}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag_name,) = path.into_inner();
|
||||
|
||||
DbTag::delete_by_tag(&mut pool_conn, &tag_name).await?;
|
||||
Ok(HttpResponse::Ok().json("tag got deleted"))
|
||||
}
|
||||
|
||||
#[post("/tags")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestTagCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_tag = DbTag::create(&mut pool_conn, &data.tag).await?;
|
||||
|
||||
let cache = (Vec::new(), Vec::new()); // a new tag can't have any relays or schedules
|
||||
let return_tag = Tag::from_db_model_cache(&mut pool_conn, new_tag, cache)?;
|
||||
Ok(HttpResponse::Created().json(return_tag))
|
||||
}
|
114
src/handlers/v1/ws/controllers/handlers.rs
Normal file
114
src/handlers/v1/ws/controllers/handlers.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use actix::{Actor, AsyncContext};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{Controller, FromDbModel};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
|
||||
use emgauwa_common::utils;
|
||||
use futures::executor::block_on;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::Sqlite;
|
||||
|
||||
use crate::app_state::{Action, ConnectController, UpdateRelayStates};
|
||||
use crate::handlers::v1::ws::controllers::ControllersWs;
|
||||
|
||||
impl ControllersWs {
|
||||
pub fn handle_register(
|
||||
&mut self,
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
ctx: &mut <ControllersWs as Actor>::Context,
|
||||
controller: Controller,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
log::info!(
|
||||
"Registering controller: {} ({})",
|
||||
controller.c.name,
|
||||
controller.c.uid
|
||||
);
|
||||
let c = &controller.c;
|
||||
let controller_db = block_on(DbController::get_by_uid_or_create(
|
||||
conn,
|
||||
&c.uid,
|
||||
&c.name,
|
||||
c.relay_count,
|
||||
))?;
|
||||
block_on(controller_db.update_active(conn, true))?;
|
||||
// update only the relay count
|
||||
block_on(controller_db.update(conn, &controller_db.name, c.relay_count))?;
|
||||
|
||||
for relay in &controller.relays {
|
||||
log::debug!(
|
||||
"Registering relay: {} ({})",
|
||||
relay.r.name,
|
||||
match relay.is_on {
|
||||
Some(true) => "+",
|
||||
Some(false) => "-",
|
||||
None => "?",
|
||||
}
|
||||
);
|
||||
let (new_relay, created) = block_on(DbRelay::get_by_controller_and_num_or_create(
|
||||
conn,
|
||||
&controller_db,
|
||||
relay.r.number,
|
||||
&relay.r.name,
|
||||
))?;
|
||||
if created {
|
||||
let mut relay_schedules = Vec::new();
|
||||
for schedule in &relay.schedules {
|
||||
let (new_schedule, _) = block_on(DbSchedule::get_by_uid_or_create(
|
||||
conn,
|
||||
schedule.uid.clone(),
|
||||
&schedule.name,
|
||||
&schedule.periods,
|
||||
))?;
|
||||
relay_schedules.push(new_schedule);
|
||||
}
|
||||
|
||||
block_on(DbJunctionRelaySchedule::set_schedules(
|
||||
conn,
|
||||
&new_relay,
|
||||
relay_schedules.iter().collect(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
let controller_uid = &controller.c.uid;
|
||||
let controller_db = block_on(DbController::get_by_uid(conn, controller_uid))?
|
||||
.ok_or(DatabaseError::InsertGetError)?;
|
||||
let controller = Controller::from_db_model(conn, controller_db)?;
|
||||
|
||||
let addr = ctx.address();
|
||||
self.controller_uid = Some(controller_uid.clone());
|
||||
block_on(self.app_state.send(ConnectController {
|
||||
address: addr.recipient(),
|
||||
controller: controller.clone(),
|
||||
}))??;
|
||||
|
||||
block_on(self.app_state.send(Action {
|
||||
controller_uid: controller_uid.clone(),
|
||||
action: ControllerWsAction::Controller(controller.clone()),
|
||||
}))??;
|
||||
block_on(self.app_state.send(Action {
|
||||
controller_uid: controller_uid.clone(),
|
||||
action: ControllerWsAction::Relays(controller.relays),
|
||||
}))??;
|
||||
|
||||
log::debug!("Done registering controller");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_relay_states(
|
||||
&mut self,
|
||||
controller_uid: EmgauwaUid,
|
||||
relay_states: RelayStates,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
log::debug!(
|
||||
"Received relay states: {} for {}",
|
||||
utils::printable_relay_states(&relay_states),
|
||||
controller_uid
|
||||
);
|
||||
block_on(self.app_state.send(UpdateRelayStates {
|
||||
controller_uid,
|
||||
relay_states,
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
154
src/handlers/v1/ws/controllers/mod.rs
Normal file
154
src/handlers/v1/ws/controllers/mod.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
mod handlers;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
|
||||
use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::ProtocolError;
|
||||
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid};
|
||||
use futures::executor::block_on;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use ws::Message;
|
||||
|
||||
use crate::app_state::{AppState, DisconnectController};
|
||||
use crate::utils::flatten_result;
|
||||
|
||||
pub struct ControllersWs {
|
||||
pub pool: Pool<Sqlite>,
|
||||
pub controller_uid: Option<EmgauwaUid>,
|
||||
pub app_state: Addr<AppState>,
|
||||
pub hb: Instant,
|
||||
}
|
||||
|
||||
impl Actor for ControllersWs {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
self.hb(ctx);
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
if let Some(controller_uid) = &self.controller_uid {
|
||||
let flat_res = flatten_result(
|
||||
block_on(self.app_state.send(DisconnectController {
|
||||
controller_uid: controller_uid.clone(),
|
||||
}))
|
||||
.map_err(EmgauwaError::from),
|
||||
);
|
||||
if let Err(err) = flat_res {
|
||||
log::error!("Error disconnecting controller: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControllersWs {
|
||||
pub fn handle_action(
|
||||
&mut self,
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
ctx: &mut <ControllersWs as Actor>::Context,
|
||||
action: ControllerWsAction,
|
||||
) {
|
||||
let action_res = match action {
|
||||
ControllerWsAction::Register(controller) => self.handle_register(conn, ctx, controller),
|
||||
ControllerWsAction::RelayStates((controller_uid, relay_states)) => {
|
||||
self.handle_relay_states(controller_uid, relay_states)
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
if let Err(e) = action_res {
|
||||
log::error!("Error handling action: {:?}", e);
|
||||
ctx.text(
|
||||
serde_json::to_string(&e).unwrap_or(format!("Error in handling action: {:?}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
|
||||
log::warn!("Websocket Controller heartbeat failed, disconnecting!");
|
||||
ctx.stop();
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping(&[]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ControllerWsAction> for ControllersWs {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, action: ControllerWsAction, ctx: &mut Self::Context) -> Self::Result {
|
||||
match action {
|
||||
ControllerWsAction::Disconnect => {
|
||||
ctx.close(None);
|
||||
ctx.stop();
|
||||
}
|
||||
_ => {
|
||||
let action_json = serde_json::to_string(&action)?;
|
||||
ctx.text(action_json);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<Message, ProtocolError>> for ControllersWs {
|
||||
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
|
||||
let mut pool_conn = match block_on(self.pool.acquire()) {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
log::error!("Failed to acquire database connection: {:?}", err);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let msg = match msg {
|
||||
Err(_) => {
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg)
|
||||
}
|
||||
Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
Message::Text(text) => match serde_json::from_str(&text) {
|
||||
Ok(action) => {
|
||||
self.handle_action(&mut pool_conn, ctx, action);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing action: {:?}", e);
|
||||
ctx.text(
|
||||
serde_json::to_string(&EmgauwaError::Serialization(e))
|
||||
.unwrap_or(String::from("Error in deserializing action")),
|
||||
);
|
||||
}
|
||||
},
|
||||
Message::Binary(_) => log::warn!("Received unexpected binary in controller ws"),
|
||||
Message::Close(reason) => {
|
||||
ctx.close(reason);
|
||||
ctx.stop();
|
||||
}
|
||||
Message::Continuation(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
Message::Nop => (),
|
||||
}
|
||||
}
|
||||
}
|
53
src/handlers/v1/ws/mod.rs
Normal file
53
src/handlers/v1/ws/mod.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::handlers::v1::ws::controllers::ControllersWs;
|
||||
use crate::handlers::v1::ws::relays::RelaysWs;
|
||||
|
||||
pub mod controllers;
|
||||
pub mod relays;
|
||||
|
||||
#[get("/ws/controllers")]
|
||||
pub async fn ws_controllers(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let resp = ws::start(
|
||||
ControllersWs {
|
||||
pool: pool.get_ref().clone(),
|
||||
controller_uid: None,
|
||||
app_state: app_state.get_ref().clone(),
|
||||
hb: Instant::now(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
|
||||
resp
|
||||
}
|
||||
|
||||
#[get("/ws/relays")]
|
||||
pub async fn ws_relays(
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let resp = ws::start(
|
||||
RelaysWs {
|
||||
app_state: app_state.get_ref().clone(),
|
||||
hb: Instant::now(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
|
||||
resp
|
||||
}
|
106
src/handlers/v1/ws/relays/mod.rs
Normal file
106
src/handlers/v1/ws/relays/mod.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, StreamHandler};
|
||||
use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::ProtocolError;
|
||||
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use futures::executor::block_on;
|
||||
|
||||
use crate::app_state::{AppState, ConnectRelayClient};
|
||||
|
||||
pub struct RelaysWs {
|
||||
pub app_state: Addr<AppState>,
|
||||
pub hb: Instant,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct SendRelays {
|
||||
pub relays_json: String,
|
||||
}
|
||||
|
||||
impl Actor for RelaysWs {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// get unique id for ctx
|
||||
match self.get_relays_json() {
|
||||
Ok(relays_json) => {
|
||||
ctx.text(relays_json);
|
||||
self.hb(ctx);
|
||||
|
||||
block_on(self.app_state.send(ConnectRelayClient {
|
||||
addr: ctx.address(),
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Error getting relays: {:?}", err);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelaysWs {
|
||||
fn get_relays_json(&self) -> Result<String, EmgauwaError> {
|
||||
let relays = block_on(self.app_state.send(crate::app_state::GetRelays {}))??;
|
||||
serde_json::to_string(&relays).map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
|
||||
log::debug!("Websocket Relay heartbeat failed, disconnecting!");
|
||||
ctx.stop();
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping(&[]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ProtocolError>> for RelaysWs {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ProtocolError>, ctx: &mut Self::Context) {
|
||||
let msg = match msg {
|
||||
Err(_) => {
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg)
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(_) => log::debug!("Received unexpected text in relays ws"),
|
||||
ws::Message::Binary(_) => log::debug!("Received unexpected binary in relays ws"),
|
||||
ws::Message::Close(reason) => {
|
||||
ctx.close(reason);
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Continuation(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Nop => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<SendRelays> for RelaysWs {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: SendRelays, ctx: &mut Self::Context) -> Self::Result {
|
||||
ctx.text(msg.relays_json);
|
||||
}
|
||||
}
|
124
src/main.rs
Normal file
124
src/main.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use std::net::TcpListener;
|
||||
|
||||
use actix::{Actor, Arbiter};
|
||||
use actix_cors::Cors;
|
||||
use actix_web::middleware::TrailingSlash;
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::utils::{drop_privileges, init_logging};
|
||||
use serde_json::json;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
mod app_state;
|
||||
mod handlers;
|
||||
mod settings;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
let settings = settings::init()?;
|
||||
|
||||
let listener = TcpListener::bind(format!("{}:{}", settings.server.host, settings.server.port))?;
|
||||
drop_privileges(&settings.permissions)?;
|
||||
|
||||
init_logging(&settings.logging.level)?;
|
||||
|
||||
let pool = emgauwa_common::db::init(&settings.database).await?;
|
||||
|
||||
let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?;
|
||||
DbController::all_inactive(&mut conn)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?;
|
||||
conn.close().await.map_err(EmgauwaError::from)?;
|
||||
|
||||
let app_state_arbiter = Arbiter::with_tokio_rt(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
let app_state_pool = pool.clone();
|
||||
let app_state = Actor::start_in_arbiter(&app_state_arbiter.handle(), move |_| {
|
||||
AppState::new(app_state_pool)
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"Starting server on {}:{}",
|
||||
settings.server.host,
|
||||
settings.server.port
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default().allow_any_method().allow_any_header();
|
||||
|
||||
let origins = settings.origins.clone();
|
||||
let cors = match settings.origins.is_empty() {
|
||||
true => cors.allow_any_origin(),
|
||||
false => cors.allowed_origin_fn(move |origin, _req_head| {
|
||||
origins.contains(&origin.to_str().unwrap_or_default().to_string())
|
||||
}),
|
||||
};
|
||||
|
||||
let api_default = json!({
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "0.0.0",
|
||||
"title": "Failed to load API documentation",
|
||||
}
|
||||
});
|
||||
let api_v1_json =
|
||||
serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/api.v1.json")))
|
||||
.unwrap_or(api_default.clone());
|
||||
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.wrap(middleware::Logger::default())
|
||||
.app_data(web::JsonConfig::default().error_handler(handlers::json_error_handler))
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
.app_data(web::Data::new(app_state.clone()))
|
||||
.service(
|
||||
SwaggerUi::new("/api/docs/{_:.*}")
|
||||
.external_urls_from_iter_unchecked([("/api/v1.json", api_v1_json)]),
|
||||
)
|
||||
.service(
|
||||
web::scope("/api/v1")
|
||||
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
|
||||
.service(handlers::v1::controllers::index)
|
||||
.service(handlers::v1::controllers::show)
|
||||
.service(handlers::v1::controllers::update)
|
||||
.service(handlers::v1::controllers::delete)
|
||||
.service(handlers::v1::relays::index)
|
||||
.service(handlers::v1::relays::tagged)
|
||||
.service(handlers::v1::relays::index_for_controller)
|
||||
.service(handlers::v1::relays::show_for_controller)
|
||||
.service(handlers::v1::relays::update_for_controller)
|
||||
.service(handlers::v1::relays::pulse)
|
||||
.service(handlers::v1::schedules::index)
|
||||
.service(handlers::v1::schedules::tagged)
|
||||
.service(handlers::v1::schedules::show)
|
||||
.service(handlers::v1::schedules::add)
|
||||
.service(handlers::v1::schedules::add_list)
|
||||
.service(handlers::v1::schedules::update)
|
||||
.service(handlers::v1::schedules::delete)
|
||||
.service(handlers::v1::tags::index)
|
||||
.service(handlers::v1::tags::show)
|
||||
.service(handlers::v1::tags::delete)
|
||||
.service(handlers::v1::tags::add)
|
||||
.service(handlers::v1::macros::index)
|
||||
.service(handlers::v1::macros::show)
|
||||
.service(handlers::v1::macros::add)
|
||||
.service(handlers::v1::macros::update)
|
||||
.service(handlers::v1::macros::delete)
|
||||
.service(handlers::v1::macros::execute)
|
||||
.service(handlers::v1::ws::ws_controllers)
|
||||
.service(handlers::v1::ws::ws_relays),
|
||||
)
|
||||
})
|
||||
.listen(listener)?
|
||||
.run()
|
||||
.await
|
||||
}
|
32
src/settings.rs
Normal file
32
src/settings.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::settings;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub struct Settings {
|
||||
pub server: settings::Server,
|
||||
pub database: String,
|
||||
pub permissions: settings::Permissions,
|
||||
pub logging: settings::Logging,
|
||||
|
||||
pub origins: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
server: settings::Server::default(),
|
||||
database: String::from("sqlite://emgauwa-core.sqlite"),
|
||||
permissions: settings::Permissions::default(),
|
||||
logging: settings::Logging::default(),
|
||||
|
||||
origins: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Result<Settings, EmgauwaError> {
|
||||
settings::load("core", "CORE")
|
||||
}
|
7
src/utils.rs
Normal file
7
src/utils.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> {
|
||||
match res {
|
||||
Ok(Ok(t)) => Ok(t),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue