Refactor websocket

This commit is contained in:
Tobias Reisinger 2026-09-02 00:25:56 +02:00
commit f3d367e479
Signed by: serguzim
GPG key ID: 13AD60C237A28DFE
22 changed files with 924 additions and 773 deletions

2
.gitignore vendored
View file

@ -6,5 +6,5 @@
/emgauwa-*.sqlite
/emgauwa-*.sqlite-*
/emgauwa-*.json
/emgauwa-*.cfg.*

151
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "actix"
@ -89,7 +89,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand",
"rand 0.8.5",
"sha1",
"smallvec",
"tokio",
@ -213,24 +213,6 @@ dependencies = [
"url",
]
[[package]]
name = "actix-web-actors"
version = "4.3.1+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980"
dependencies = [
"actix",
"actix-codec",
"actix-http",
"actix-web",
"bytes",
"bytestring",
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-web-codegen"
version = "4.3.0"
@ -243,6 +225,20 @@ dependencies = [
"syn",
]
[[package]]
name = "actix-ws"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99"
dependencies = [
"actix-codec",
"actix-http",
"actix-web",
"bytestring",
"futures-core",
"tokio",
]
[[package]]
name = "actix_derive"
version = "0.6.2"
@ -276,7 +272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
@ -558,7 +554,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom",
"getrandom 0.2.15",
"once_cell",
"tiny-keccak",
]
@ -813,6 +809,7 @@ dependencies = [
"serde_with",
"simple_logger",
"sqlx",
"tokio",
"uuid",
]
@ -823,11 +820,12 @@ dependencies = [
"actix",
"actix-cors",
"actix-web",
"actix-web-actors",
"actix-ws",
"emgauwa-common",
"futures",
"itertools",
"log",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
@ -1035,7 +1033,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.7+wasi-0.2.4",
]
[[package]]
@ -1539,7 +1549,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@ -1555,7 +1565,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand",
"rand 0.8.5",
"smallvec",
"zeroize",
]
@ -1803,6 +1813,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
@ -1810,8 +1826,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
@ -1821,7 +1847,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
@ -1830,7 +1866,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
@ -1902,7 +1947,7 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
@ -2123,7 +2168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@ -2305,7 +2350,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
@ -2343,7 +2388,7 @@ dependencies = [
"md-5",
"memchr",
"once_cell",
"rand",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
@ -2531,9 +2576,21 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
@ -2751,7 +2808,7 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
@ -2782,6 +2839,24 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
@ -3036,6 +3111,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "write16"
version = "1.0.0"

View file

@ -9,7 +9,7 @@ emgauwa-common = { git = "https://git.serguzim.me/emgauwa/common.git" }
actix = "0.13"
actix-web = "4.4"
actix-web-actors = "4.2"
actix-ws = "0.3"
actix-cors = "0.7"
utoipa = "5.3"
@ -26,4 +26,5 @@ serde_derive = "1.0"
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros"] }
futures = "0.3"
tokio = { version = "1.36", features = ["rt", "rt-multi-thread"] }
tokio = { version = "1.36", features = ["macros", "rt", "rt-multi-thread"] }
rand = "0.9.2"

View file

@ -1,14 +1,14 @@
build-rpi:
cross build --target arm-unknown-linux-gnueabihf
emgauwa-core.json: core.pkl
emgauwa-core.cfg.json: core.pkl
pkl eval -f json -o $@ $<
config:
$(MAKE) emgauwa-core.json
$(MAKE) emgauwa-core.cfg.json
clean:
rm -f emgauwa-core.json
rm -f emgauwa-core.cfg.*
rm -f emgauwa-core.sqlite
emgauwa-core_%:

View file

@ -1,7 +1,11 @@
amends "package://emgauwa.app/pkl/emgauwa@0.2.1#/core.pkl"
amends "package://emgauwa.app/pkl/emgauwa@0.2.3#/core.pkl"
database = "sqlite://emgauwa-core.sqlite"
server {
token = "dev tokenx"
host = "127.0.0.1"
port = 4419
token = "dev token"
}
logging {

View file

@ -1,5 +1,5 @@
variable "REG" {
default = "registry.serguzim.me"
default = "rg.nl-ams.scw.cloud/public.serguzim.net"
}
variable "REPO" {
default = "emgauwa/core"

14
run-coverage.sh Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
export RUSTFLAGS="-C instrument-coverage"
cargo run
llvm-profdata merge -sparse default_*.profraw -o emgauwa-core.profdata
llvm-cov report \
--object ./target/debug/emgauwa-core \
--instr-profile=emgauwa-core.profdata \
--ignore-filename-regex="${CARGO_HOME:-.cargo/registry}" \
--use-color \
--summary-only

View file

@ -1,189 +0,0 @@
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);
}
self.notify_relay_clients();
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);
}
}

View file

@ -1,14 +1,12 @@
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 emgauwa_common::types::{ControllersWsAction, EmgauwaUid, RequestControllerUpdate};
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
use tokio::sync::mpsc;
use crate::handlers::EmgauwaMessage;
use crate::server::WsServerAction;
#[get("/controllers")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
@ -17,7 +15,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw
let db_controllers = DbController::get_all(&mut pool_conn).await?;
let controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(controllers))
}
@ -36,13 +34,14 @@ pub async fn show(
.ok_or(DatabaseError::NotFound)?;
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
pool_conn.close().await?;
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>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String,)>,
data: web::Json<RequestControllerUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
@ -60,13 +59,9 @@ pub async fn update(
.await?;
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
pool_conn.close().await?;
app_state
.send(app_state::Action {
controller_uid: uid.clone(),
action: ControllerWsAction::Controller(return_controller.clone()),
})
.await??;
ws_server_tx.send(WsServerAction::ForwardToController(uid, ControllersWsAction::Controller(return_controller.clone())))?;
Ok(HttpResponse::Ok().json(return_controller))
}
@ -74,7 +69,7 @@ pub async fn update(
#[delete("/controllers/{controller_id}")]
pub async fn delete(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
@ -82,12 +77,11 @@ pub async fn delete(
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??;
log::debug!("Deleting controller with uid: {}", uid.clone());
DbController::delete_by_uid(&mut pool_conn, uid.clone()).await?;
pool_conn.close().await?;
ws_server_tx.send(WsServerAction::DisconnectController(uid.clone(), false))?;
DbController::delete_by_uid(&mut pool_conn, uid).await?;
Ok(HttpResponse::Ok().emgauwa_message("controller got deleted"))
}

View file

@ -1,17 +1,15 @@
use actix::Addr;
use actix_web::{delete, get, post, put, web, HttpResponse};
use emgauwa_common::db::{DbController, 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,
ControllersWsAction, EmgauwaUid, RequestMacroCreate, RequestMacroExecute, RequestMacroUpdate,
};
use sqlx::pool::PoolConnection;
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
use tokio::sync::mpsc;
use crate::handlers::EmgauwaMessage;
use crate::server::WsServerAction;
#[get("/macros")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
@ -19,7 +17,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw
let db_macros = DbMacro::get_all(&mut pool_conn).await?;
let macros: Vec<Macro> = convert_db_list(&mut pool_conn, db_macros)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(macros))
}
@ -38,6 +36,7 @@ pub async fn show(
.ok_or(DatabaseError::NotFound)?;
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(return_macro))
}
@ -55,6 +54,7 @@ pub async fn add(
.await?;
let return_macro = Macro::from_db_model(&mut pool_conn, new_macro)?;
pool_conn.close().await?;
Ok(HttpResponse::Created().json(return_macro))
}
@ -84,6 +84,7 @@ pub async fn update(
}
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(return_macro))
}
@ -98,14 +99,15 @@ pub async fn delete(
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
DbMacro::delete_by_uid(&mut pool_conn, uid).await?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().emgauwa_message("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,)>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
query: web::Query<RequestMacroExecute>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
@ -137,14 +139,10 @@ pub async fn execute(
let affected_relays =
collect_affected_relays(&mut pool_conn, &mut actions, &controller).await?;
app_state
.send(app_state::Action {
controller_uid: controller.uid,
action: ControllerWsAction::Relays(affected_relays.clone()),
})
.await??;
ws_server_tx.send(WsServerAction::ForwardToController(uid.clone(), ControllersWsAction::Relays(affected_relays.clone())))?;
}
pool_conn.close().await?;
Ok(HttpResponse::Ok().emgauwa_message("macro got executed"))
}

View file

@ -1,42 +1,55 @@
use actix::Addr;
use crate::handlers::EmgauwaMessage;
use crate::server::WsServerAction;
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::types::{
ControllersWsAction, EmgauwaUid, RequestRelayPulse, RequestRelayUpdate,
};
use sqlx::{Pool, Sqlite};
use tokio::sync::{mpsc, oneshot};
use crate::app_state;
use crate::app_state::AppState;
use crate::handlers::EmgauwaMessage;
pub async fn get_stated_relays(app_state: &Addr<AppState>) -> Result<Vec<Relay>, EmgauwaError> {
app_state.send(app_state::GetRelays {}).await?
pub async fn get_stated_relays(
ws_server_tx: &web::Data<mpsc::UnboundedSender<WsServerAction>>,
) -> Result<Vec<Relay>, EmgauwaError> {
let (res_tx, res_rx) = oneshot::channel();
ws_server_tx.send(WsServerAction::GetRelays(res_tx))?;
res_rx.await?
}
pub async fn load_state_for_relay(relay: &mut Relay, app_state: &Addr<AppState>) -> Result<(), EmgauwaError>{
let stated_relays = get_stated_relays(app_state).await?;
pub async fn load_state_for_relay(
relay: &mut Relay,
ws_server_tx: &web::Data<mpsc::UnboundedSender<WsServerAction>>,
) -> Result<(), EmgauwaError> {
let stated_relays = get_stated_relays(ws_server_tx).await?;
relay.find_and_apply_state(&stated_relays);
Ok(())
}
pub async fn load_state_for_relays(relays: &mut [Relay], app_state: &Addr<AppState>) -> Result<(), EmgauwaError>{
let stated_relays = get_stated_relays(app_state).await?;
relays.iter_mut().for_each(|r| r.find_and_apply_state(&stated_relays));
pub async fn load_state_for_relays(
relays: &mut [Relay],
ws_server_tx: &web::Data<mpsc::UnboundedSender<WsServerAction>>,
) -> Result<(), EmgauwaError> {
let stated_relays = get_stated_relays(ws_server_tx).await?;
relays
.iter_mut()
.for_each(|r| r.find_and_apply_state(&stated_relays));
Ok(())
}
#[get("/relays")]
pub async fn index(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
let db_relays = DbRelay::get_all(&mut pool_conn).await?;
let mut relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
load_state_for_relays(&mut relays, &app_state).await?;
pool_conn.close().await?;
load_state_for_relays(&mut relays, &ws_server_tx).await?;
Ok(HttpResponse::Ok().json(relays))
}
@ -44,7 +57,7 @@ pub async fn index(
#[get("/relays/tag/{tag}")]
pub async fn tagged(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
@ -57,7 +70,8 @@ pub async fn tagged(
let db_relays = DbRelay::get_by_tag(&mut pool_conn, &tag_db).await?;
let mut relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
load_state_for_relays(&mut relays, &app_state).await?;
pool_conn.close().await?;
load_state_for_relays(&mut relays, &ws_server_tx).await?;
Ok(HttpResponse::Ok().json(relays))
}
@ -65,7 +79,7 @@ pub async fn tagged(
#[get("/controllers/{controller_id}/relays")]
pub async fn index_for_controller(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String,)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
@ -80,7 +94,8 @@ pub async fn index_for_controller(
let db_relays = controller.get_relays(&mut pool_conn).await?;
let mut relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
load_state_for_relays(&mut relays, &app_state).await?;
pool_conn.close().await?;
load_state_for_relays(&mut relays, &ws_server_tx).await?;
Ok(HttpResponse::Ok().json(relays))
}
@ -88,7 +103,7 @@ pub async fn index_for_controller(
#[get("/controllers/{controller_id}/relays/{relay_num}")]
pub async fn show_for_controller(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String, i64)>,
) -> Result<HttpResponse, EmgauwaError> {
let mut pool_conn = pool.acquire().await?;
@ -105,7 +120,8 @@ pub async fn show_for_controller(
.ok_or(DatabaseError::NotFound)?;
let mut relay = Relay::from_db_model(&mut pool_conn, db_relay)?;
load_state_for_relay(&mut relay, &app_state).await?;
pool_conn.close().await?;
load_state_for_relay(&mut relay, &ws_server_tx).await?;
Ok(HttpResponse::Ok().json(relay))
}
@ -113,7 +129,7 @@ pub async fn show_for_controller(
#[put("/controllers/{controller_id}/relays/{relay_num}")]
pub async fn update_for_controller(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String, i64)>,
data: web::Json<RequestRelayUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
@ -158,24 +174,25 @@ pub async fn update_for_controller(
let mut return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
load_state_for_relay(&mut return_relay, &app_state).await?;
load_state_for_relay(&mut return_relay, &ws_server_tx).await?;
match &data.override_schedule {
Some(Some(s_uid)) => { // We want to set an override schedule
Some(Some(s_uid)) => {
// We want to set an override schedule
let schedule = s_uid.get_schedule(&mut pool_conn).await?;
return_relay.override_schedule = Some(schedule);
}
Some(None) => { // We want to unset the override schedule
Some(None) => {
// We want to unset the override schedule
return_relay.override_schedule = None;
}
None => {} // We want to keep the override schedule as is
}
app_state
.send(app_state::Action {
controller_uid: uid,
action: ControllerWsAction::Relays(vec![return_relay.clone()]),
})
.await??;
pool_conn.close().await?;
ws_server_tx.send(WsServerAction::ForwardToController(
uid,
ControllersWsAction::Relays(vec![return_relay.clone()]),
))?;
Ok(HttpResponse::Ok().json(return_relay))
}
@ -183,7 +200,7 @@ pub async fn update_for_controller(
#[post("/controllers/{controller_id}/relays/{relay_num}/pulse")]
pub async fn pulse(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String, i64)>,
data: web::Json<RequestRelayPulse>,
) -> Result<HttpResponse, EmgauwaError> {
@ -200,14 +217,13 @@ pub async fn pulse(
.await?
.ok_or(DatabaseError::NotFound)?;
let duration = data.duration.filter(|&d| d > 0);
pool_conn.close().await?;
app_state
.send(app_state::Action {
controller_uid: uid,
action: ControllerWsAction::RelayPulse((relay.number, duration)),
})
.await??;
let duration = data.duration.filter(|&d| d > 0);
ws_server_tx.send(WsServerAction::ForwardToController(
uid,
ControllersWsAction::RelayPulse((relay.number, duration)),
))?;
Ok(HttpResponse::Ok().emgauwa_message("pulse sent"))
}

View file

@ -1,19 +1,17 @@
use actix::Addr;
use crate::handlers::EmgauwaMessage;
use crate::server::WsServerAction;
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, RequestScheduleGetTagged, RequestScheduleUpdate,
ControllersWsAction, RequestScheduleCreate, RequestScheduleGetTagged, RequestScheduleUpdate,
ScheduleUid,
};
use itertools::Itertools;
use sqlx::pool::PoolConnection;
use sqlx::{Pool, Sqlite};
use crate::app_state;
use crate::app_state::AppState;
use crate::handlers::EmgauwaMessage;
use tokio::sync::mpsc;
#[get("/schedules")]
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
@ -21,7 +19,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw
let db_schedules = DbSchedule::get_all(&mut pool_conn).await?;
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(schedules))
}
@ -53,7 +51,7 @@ pub async fn tagged(
}
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(schedules))
}
@ -72,6 +70,7 @@ pub async fn show(
.ok_or(DatabaseError::NotFound)?;
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(return_schedule))
}
@ -97,6 +96,7 @@ pub async fn add(
}
let return_schedule = Schedule::from_db_model(&mut pool_conn, new_schedule)?;
pool_conn.close().await?;
Ok(HttpResponse::Created().json(return_schedule))
}
@ -133,13 +133,14 @@ pub async fn add_list(
}
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
pool_conn.close().await?;
Ok(HttpResponse::Created().json(schedules))
}
#[put("/schedules/{schedule_id}")]
pub async fn update(
pool: web::Data<Pool<Sqlite>>,
app_state: web::Data<Addr<AppState>>,
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
path: web::Path<(String,)>,
data: web::Json<RequestScheduleUpdate>,
) -> Result<HttpResponse, EmgauwaError> {
@ -179,15 +180,15 @@ pub async fn update(
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??;
ws_server_tx
.send(WsServerAction::ForwardToController(
controller.uid,
ControllersWsAction::Schedules(vec![schedule.clone()]),
))?;
}
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(return_schedule))
}
@ -196,8 +197,6 @@ 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())?;
@ -205,7 +204,9 @@ pub async fn delete(
ScheduleUid::Off => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
ScheduleUid::On => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
ScheduleUid::Any(_) => {
let mut pool_conn = pool.acquire().await?;
DbSchedule::delete_by_uid(&mut pool_conn, uid).await?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().emgauwa_message("schedule got deleted"))
}
}

View file

@ -12,6 +12,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw
let mut pool_conn = pool.acquire().await?;
let db_tags = DbTag::get_all(&mut pool_conn).await?;
pool_conn.close().await?;
let tags: Vec<String> = db_tags.iter().map(|t| t.tag.clone()).collect();
@ -32,6 +33,7 @@ pub async fn show(
.ok_or(DatabaseError::NotFound)?;
let return_tag = Tag::from_db_model(&mut pool_conn, tag)?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().json(return_tag))
}
@ -45,6 +47,7 @@ pub async fn delete(
let (tag_name,) = path.into_inner();
DbTag::delete_by_tag(&mut pool_conn, &tag_name).await?;
pool_conn.close().await?;
Ok(HttpResponse::Ok().emgauwa_message("tag got deleted"))
}
@ -59,5 +62,6 @@ pub async fn add(
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)?;
pool_conn.close().await?;
Ok(HttpResponse::Created().json(return_tag))
}

View file

@ -0,0 +1,114 @@
use std::time::Instant;
use actix_ws::{AggregatedMessage, CloseCode, CloseReason};
use futures::StreamExt;
use tokio::sync::{mpsc, oneshot};
use tokio::time::interval;
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
use crate::server::{WsConnId, WsControllerAction, WsServerAction};
#[derive(Debug, Clone)]
pub struct ControllerWsHandle {
pub cmd_tx: mpsc::UnboundedSender<WsServerAction>,
}
impl ControllerWsHandle {
/// Register client message sender and obtain connection ID.
pub async fn connect(&self, conn_tx: mpsc::UnboundedSender<WsControllerAction>) -> WsConnId {
let (res_tx, res_rx) = oneshot::channel();
// unwrap: chat server should not have been dropped
self.cmd_tx
.send(WsServerAction::ConnectController(conn_tx, res_tx))
.unwrap();
// unwrap: chat server does not drop out the response channel
res_rx.await.unwrap()
}
/// Unregister message sender and broadcast disconnection message to current room.
pub fn disconnect(&self, conn: WsConnId) {
// unwrap: chat server should not have been dropped
self.cmd_tx.send(WsServerAction::DisconnectControllerConn(conn, true)).unwrap();
}
pub async fn message(&self, conn: WsConnId, msg: String) {
self.cmd_tx.send(WsServerAction::ControllerMessage(conn, msg)).unwrap();
}
pub async fn run(
&self,
mut session: actix_ws::Session,
msg_stream: actix_ws::MessageStream,
) {
log::info!("connected");
let mut last_heartbeat = Instant::now();
let mut interval = interval(HEARTBEAT_INTERVAL);
let (conn_tx, mut conn_rx) = mpsc::unbounded_channel();
// unwrap: chat server is not dropped before the HTTP server
let conn_id = self.connect(conn_tx).await;
let mut msg_stream = msg_stream
.max_frame_size(128 * 1024)
.aggregate_continuations()
.max_continuation_size(2 * 1024 * 1024);
let close_reason = loop {
tokio::select! {
Some(Ok(msg)) = msg_stream.next() => {
match msg {
AggregatedMessage::Ping(bytes) => {
last_heartbeat = Instant::now();
session.pong(&bytes).await.unwrap();
}
AggregatedMessage::Pong(_) => {
last_heartbeat = Instant::now();
}
AggregatedMessage::Text(text) => {
let text_str = text.to_string();
log::debug!("ws msg: {}", text_str);
self.message(conn_id, text_str).await;
}
AggregatedMessage::Binary(_) => {
log::warn!("unexpected binary message");
}
AggregatedMessage::Close(reason) => break reason,
}
}
Some(chat_msg) = conn_rx.recv() => {
match chat_msg {
WsControllerAction::Forward(msg) => {
session.text(msg).await.unwrap();
}
WsControllerAction::Disconnect => {
break Some(CloseReason::from((CloseCode::Normal, "Disconnect wanted by server")));
}
}
}
_ = interval.tick() => {
if Instant::now().duration_since(last_heartbeat) > HEARTBEAT_TIMEOUT {
break None;
}
let _ = session.ping(b"").await;
}
else => {
break None;
}
}
};
// attempt to close the connection gracefully
log::debug!("Closing a controller connection: {:?}", close_reason);
let _ = session.close(close_reason).await;
self.disconnect(conn_id);
}
}

View file

@ -1,114 +0,0 @@
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(())
}
}

View file

@ -1,153 +1,53 @@
use std::time::Instant;
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
use actix_web_actors::ws;
use actix_web_actors::ws::ProtocolError;
use futures::executor::block_on;
use sqlx::{Pool, Sqlite};
use sqlx::pool::PoolConnection;
use ws::Message;
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
use actix_web::{get, web, HttpRequest, HttpResponse};
use tokio::sync::mpsc;
use tokio::task::spawn_local;
use emgauwa_common::constants;
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid};
use crate::handlers::v1::ws::controllers::handle::ControllerWsHandle;
use crate::server::WsServerAction;
use crate::settings::Settings;
use crate::app_state::{AppState, DisconnectController};
mod handle;
mod handlers;
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 {
if let Err(err) = block_on(self.app_state.send(DisconnectController {
controller_uid: controller_uid.clone(),
})).unwrap_or_else(|err| Err(EmgauwaError::from(err))) {
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(()),
pub async fn run_ws_server_handle(ws_server_tx: mpsc::UnboundedSender<WsServerAction>, session: actix_ws::Session, msg_stream: actix_ws::MessageStream) {
let ws_server_handle = ControllerWsHandle {
cmd_tx: ws_server_tx,
};
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;
}
log::trace!("Sending ping to controller");
ctx.ping(&[]);
});
}
ws_server_handle.run(session, msg_stream).await;
}
impl Handler<ControllerWsAction> for ControllersWs {
type Result = Result<(), EmgauwaError>;
//noinspection DuplicatedCode
#[get("/ws/controllers")]
pub async fn ws_controllers(
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
settings: web::Data<Settings>,
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, EmgauwaError> {
let token = req
.headers()
.get(constants::CONTROLLER_WS_TOKEN_HEADER)
.ok_or(EmgauwaError::Unauthorized(
String::from("Missing or invalid token header"),
))?
.to_str()
.map_err(|_| EmgauwaError::Unauthorized(String::from("Invalid token header")))?;
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) => {
log::trace!("Received ping from controller: {:?}", msg);
self.hb = Instant::now();
ctx.pong(&msg)
}
Message::Pong(_) => {
log::trace!("Received pong from controller");
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 => (),
}
if token.ne(&settings.server.token) {
return Err(EmgauwaError::Unauthorized(String::from("Wrong token header")));
}
let (res, session, msg_stream) = actix_ws::handle(&req, stream).map_err(|e| {
log::error!("Error in websocket handshake: {}", e);
EmgauwaError::Internal(String::from("Error in websocket handshake"))
})?;
// spawn websocket handler (and don't await it) so that the response is returned immediately
spawn_local(run_ws_server_handle(
(**ws_server_tx).clone(),
session,
msg_stream,
));
Ok(res)
}

View file

@ -1,68 +1,4 @@
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 emgauwa_common::constants;
use crate::app_state::AppState;
use crate::handlers::v1::ws::controllers::ControllersWs;
use crate::handlers::v1::ws::relays::RelaysWs;
use crate::settings::Settings;
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>>,
settings: web::Data<Settings>,
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, EmgauwaError> {
let token = req
.headers()
.get(constants::CONTROLLER_WS_TOKEN_HEADER)
.ok_or(EmgauwaError::Unauthorized(
String::from("Missing or invalid token header"),
))?
.to_str()
.map_err(|_| EmgauwaError::Unauthorized(String::from("Invalid token header")))?;
if token != settings.server.token {
return Err(EmgauwaError::Unauthorized(String::from("Wrong token header")));
}
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
}
mod relays;
pub use relays::ws_relays;
mod controllers;
pub use controllers::ws_controllers;

View file

@ -0,0 +1,108 @@
use std::time::Instant;
use actix_ws::{AggregatedMessage, CloseCode, CloseReason};
use futures::StreamExt;
use tokio::sync::{mpsc, oneshot};
use tokio::time::interval;
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
use crate::server::{WsConnId, WsRelayAction, WsServerAction};
#[derive(Debug, Clone)]
pub struct RelayWsHandle {
pub cmd_tx: mpsc::UnboundedSender<WsServerAction>,
}
impl RelayWsHandle {
/// Register client message sender and obtain connection ID.
pub async fn connect(&self, conn_tx: mpsc::UnboundedSender<WsRelayAction>) -> WsConnId {
let (res_tx, res_rx) = oneshot::channel();
// unwrap: chat server should not have been dropped
self.cmd_tx
.send(WsServerAction::ConnectRelay(conn_tx, res_tx))
.unwrap();
// unwrap: chat server does not drop out the response channel
res_rx.await.unwrap()
}
/// Unregister message sender and broadcast disconnection message to current room.
pub fn disconnect(&self, conn: WsConnId) {
// unwrap: chat server should not have been dropped
self.cmd_tx.send(WsServerAction::DisconnectRelayConn(conn)).unwrap();
}
pub async fn run(
&self,
mut session: actix_ws::Session,
msg_stream: actix_ws::MessageStream,
) {
log::info!("connected");
let mut last_heartbeat = Instant::now();
let mut interval = interval(HEARTBEAT_INTERVAL);
let (conn_tx, mut conn_rx) = mpsc::unbounded_channel();
// unwrap: chat server is not dropped before the HTTP server
let conn_id = self.connect(conn_tx).await;
let mut msg_stream = msg_stream
.max_frame_size(128 * 1024)
.aggregate_continuations()
.max_continuation_size(2 * 1024 * 1024);
let close_reason = loop {
tokio::select! {
Some(Ok(msg)) = msg_stream.next() => {
match msg {
AggregatedMessage::Ping(bytes) => {
last_heartbeat = Instant::now();
session.pong(&bytes).await.unwrap();
}
AggregatedMessage::Pong(_) => {
last_heartbeat = Instant::now();
}
AggregatedMessage::Text(_) => {
log::warn!("unexpected text message");
}
AggregatedMessage::Binary(_) => {
log::warn!("unexpected binary message");
}
AggregatedMessage::Close(reason) => break reason,
}
}
Some(chat_msg) = conn_rx.recv() => {
match chat_msg {
WsRelayAction::Forward(msg) => {
session.text(msg).await.unwrap();
}
WsRelayAction::Disconnect => {
break Some(CloseReason::from((CloseCode::Normal, "Disconnect wanted by server")));
}
}
}
_ = interval.tick() => {
if Instant::now().duration_since(last_heartbeat) > HEARTBEAT_TIMEOUT {
break None;
}
let _ = session.ping(b"").await;
}
else => {
break None;
}
}
};
self.disconnect(conn_id);
// attempt to close the connection gracefully
let _ = session.close(close_reason).await;
}
}

View file

@ -1,109 +1,36 @@
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 actix_web::{get, web, HttpRequest, HttpResponse};
use emgauwa_common::errors::EmgauwaError;
use futures::executor::block_on;
use tokio::sync::mpsc;
use tokio::task::spawn_local;
use crate::server::WsServerAction;
use crate::app_state::{AppState, ConnectRelayClient};
mod handle;
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(),
})).map_err(
|err| log::error!("Error connecting relay-client: {:?}", err)
).ok();
}
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,
pub async fn run_ws_server_handle(ws_server_tx: mpsc::UnboundedSender<WsServerAction>, session: actix_ws::Session, msg_stream: actix_ws::MessageStream) {
let ws_server_handle = crate::handlers::v1::ws::relays::handle::RelayWsHandle {
cmd_tx: ws_server_tx,
};
match msg {
ws::Message::Ping(msg) => {
log::trace!("Received ping from relay-client: {:?}", msg);
self.hb = Instant::now();
ctx.pong(&msg)
}
ws::Message::Pong(_) => {
log::trace!("Received pong from relay-client");
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 => (),
}
}
ws_server_handle.run(session, msg_stream).await;
}
impl Handler<SendRelays> for RelaysWs {
type Result = ();
//noinspection DuplicatedCode
#[get("/ws/relays")]
pub async fn ws_relays(
ws_server_tx: web::Data<mpsc::UnboundedSender<WsServerAction>>,
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, EmgauwaError> {
let (res, session, msg_stream) = actix_ws::handle(&req, stream).map_err(|e| {
log::error!("Error in websocket handshake: {}", e);
EmgauwaError::Internal(String::from("Error in websocket handshake"))
})?;
fn handle(&mut self, msg: SendRelays, ctx: &mut Self::Context) -> Self::Result {
ctx.text(msg.relays_json);
}
// spawn websocket handler (and don't await it) so that the response is returned immediately
spawn_local(run_ws_server_handle(
(**ws_server_tx).clone(),
session,
msg_stream,
));
Ok(res)
}

View file

@ -1,6 +1,5 @@
use std::net::TcpListener;
use actix::{Actor, Arbiter};
use actix_cors::Cors;
use actix_web::middleware::TrailingSlash;
use actix_web::{middleware, web, App, HttpServer};
@ -8,13 +7,14 @@ use emgauwa_common::db::DbController;
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::utils::{drop_privileges, init_logging};
use serde_json::Value;
use tokio::spawn;
use utoipa_swagger_ui::SwaggerUi;
use crate::app_state::AppState;
use crate::server::WsServer;
mod app_state;
mod handlers;
mod settings;
mod server;
#[actix_web::main]
async fn main() -> Result<(), std::io::Error> {
@ -34,17 +34,8 @@ async fn main() -> Result<(), std::io::Error> {
.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)
});
let (ws_server, ws_server_tx) = WsServer::new(pool.clone());
let ws_server = spawn(ws_server.run());
let api_v1_json: Value =
serde_json::from_str(include_str!("../api.v1.json")).map_err(EmgauwaError::from)?;
@ -55,7 +46,7 @@ async fn main() -> Result<(), std::io::Error> {
settings.server.port
);
HttpServer::new(move || {
let http_server = HttpServer::new(move || {
let cors = Cors::default().allow_any_method().allow_any_header();
let origins = settings.origins.clone();
@ -70,8 +61,8 @@ async fn main() -> Result<(), std::io::Error> {
.wrap(cors)
.wrap(middleware::Logger::default())
.app_data(web::JsonConfig::default().error_handler(handlers::json_error_handler))
.app_data(web::Data::new(ws_server_tx.clone()))
.app_data(web::Data::new(pool.clone()))
.app_data(web::Data::new(app_state.clone()))
.app_data(web::Data::new(settings.clone()))
.service(
SwaggerUi::new("/api/docs/{_:.*}")
@ -115,6 +106,8 @@ async fn main() -> Result<(), std::io::Error> {
})
.workers((pool_size / 2) as usize)
.listen(listener)?
.run()
.await
.run();
tokio::try_join!(http_server, async move { ws_server.await.unwrap() })?;
Ok(())
}

30
src/server/mod.rs Normal file
View file

@ -0,0 +1,30 @@
use tokio::sync::{mpsc, oneshot};
use emgauwa_common::errors::EmgauwaError;
use emgauwa_common::models::Relay;
use emgauwa_common::types::{ControllersWsAction, EmgauwaUid};
mod server;
pub use server::WsServer;
pub type WsConnId = u64;
pub enum WsControllerAction {
Forward(String),
Disconnect,
}
pub enum WsRelayAction {
Forward(String),
Disconnect,
}
pub enum WsServerAction {
ConnectController(mpsc::UnboundedSender<WsControllerAction>, oneshot::Sender<WsConnId>),
DisconnectControllerConn(WsConnId, bool),
DisconnectController(EmgauwaUid, bool),
ConnectRelay(mpsc::UnboundedSender<WsRelayAction>, oneshot::Sender<WsConnId>),
DisconnectRelayConn(WsConnId),
ControllerMessage(WsConnId, String),
ForwardToController(EmgauwaUid, ControllersWsAction),
GetRelays(oneshot::Sender<Result<Vec<Relay>, EmgauwaError>>),
}

333
src/server/server.rs Normal file
View file

@ -0,0 +1,333 @@
use std::collections::HashMap;
use std::io;
use futures::executor::block_on;
use rand::random;
use sqlx::{Pool, Sqlite};
use tokio::signal;
use tokio::sync::mpsc;
use tokio::sync::mpsc::UnboundedSender;
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
use emgauwa_common::models::{convert_db_list, Controller, FromDbModel, Relay};
use emgauwa_common::types::{ControllersWsAction, EmgauwaUid, RelayStates};
use emgauwa_common::utils;
use crate::server::{WsConnId, WsControllerAction, WsRelayAction, WsServerAction};
#[derive(Debug)]
pub struct WsServer {
/// Map of connection IDs to their message receivers.
controller_sessions: HashMap<WsConnId, (mpsc::UnboundedSender<WsControllerAction>, Option<EmgauwaUid>)>,
controllers: HashMap<EmgauwaUid, (Controller, WsConnId)>,
relay_sessions: HashMap<WsConnId, mpsc::UnboundedSender<WsRelayAction>>,
/// Command receiver.
cmd_rx: mpsc::UnboundedReceiver<WsServerAction>,
pub pool: Pool<Sqlite>,
}
impl WsServer {
pub fn new(pool: Pool<Sqlite>) -> (Self, mpsc::UnboundedSender<WsServerAction>) {
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let new_self = Self {
controller_sessions: HashMap::new(),
controllers: HashMap::new(),
relay_sessions: HashMap::new(),
cmd_rx,
pool,
};
(new_self, cmd_tx)
}
/// Register new session and assign unique ID to this session
async fn connect_controller(&mut self, tx: mpsc::UnboundedSender<WsControllerAction>) -> WsConnId {
log::info!("Controller connected");
// register a session with random connection ID
let id = random::<WsConnId>();
self.controller_sessions.insert(id, (tx, None));
// send id back
id
}
async fn connect_relay(&mut self, tx: mpsc::UnboundedSender<WsRelayAction>) -> Result<WsConnId, EmgauwaError> {
log::debug!("Relay client connected");
let relays_json = serde_json::to_string(&self.get_relays()?)?;
tx.send(WsRelayAction::Forward(relays_json.clone()))?;
// register a session with random connection ID
let id = random::<WsConnId>();
self.relay_sessions.insert(id, tx);
// send id back
Ok(id)
}
async fn disconnect_controller(&mut self, sender: UnboundedSender<WsControllerAction>, emgauwa_uid: EmgauwaUid, update_db: bool) -> Result<(), EmgauwaError> {
if let Some((controller, _)) = self.controllers.remove(&emgauwa_uid) {
if update_db {
let mut pool_conn = block_on(self.pool.acquire())?;
log::debug!("Marking controller {} as inactive", controller.c.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
);
return Err(EmgauwaError::from(err));
}
}
log::debug!("Telling websocket server to disconnect a controller");
sender.send(WsControllerAction::Disconnect)?;
}
Ok(())
}
/// Unregister the connection from the controller map and broadcast a disconnection message.
async fn disconnect_controller_conn(&mut self, conn_id: WsConnId, update_db: bool) -> Result<(), EmgauwaError> {
log::debug!("Trying to find controller connection for disconnect: {}", conn_id);
if let Some((sender, Some(emgauwa_uid))) = self.controller_sessions.remove(&conn_id) {
self.disconnect_controller(sender, emgauwa_uid, update_db).await?;
}
self.notify_relay_clients()?;
Ok(())
}
async fn disconnect_relay_conn(&mut self, conn_id: WsConnId) -> Result<(), EmgauwaError> {
if let Some(relay_tx) = self.relay_sessions.remove(&conn_id) {
relay_tx.send(WsRelayAction::Disconnect)?;
}
self.notify_relay_clients()?;
Ok(())
}
fn get_relays(&mut 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)?;
controllers.iter_mut().for_each(|c| {
if let Some((cc, _)) = self.controllers.get(&c.c.uid) {
c.apply_relay_states(&cc.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) -> Result<(), EmgauwaError> {
let relays_json = serde_json::to_string(&self.get_relays()?)?;
self.relay_sessions.retain(|_, tx| {
tx.send(WsRelayAction::Forward(relays_json.clone())).is_ok()
});
Ok(())
}
// You may want to log errors rather than return them in this function.
pub async fn run(self) -> io::Result<()> {
tokio::select! {
output = self.real_run() => output,
_ = signal::ctrl_c() => Ok(())
}
}
pub async fn real_run(mut self) -> io::Result<()> {
while let Some(cmd) = self.cmd_rx.recv().await {
match cmd {
WsServerAction::ConnectController(conn_tx, res_tx) => {
let conn_id = self.connect_controller(conn_tx).await;
let _ = res_tx.send(conn_id);
}
WsServerAction::DisconnectControllerConn(conn, update_db) => {
if let Err(err) = self.disconnect_controller_conn(conn, update_db).await {
log::error!("Error disconnecting controller: {:?}", err);
}
}
WsServerAction::DisconnectController(emgauwa_uid, update_db) => {
if let Some((_, conn_id)) = self.controllers.get(&emgauwa_uid) {
if let Err(err ) = self.disconnect_controller_conn(*conn_id, update_db).await {
log::error!("Error disconnecting controller: {:?}", err);
}
}
}
WsServerAction::ControllerMessage(conn, msg) => {
match serde_json::from_str(&msg) {
Ok(action) => {
self.handle_action(conn, action);
}
Err(e) => {
log::error!("Error deserializing action: {:?}", e);
self.text_to_controller_conn(
conn,
serde_json::to_string(&EmgauwaError::Serialization(e))
.unwrap_or(String::from("Error in deserializing action")),
);
}
}
}
WsServerAction::ForwardToController(uid, action) => {
log::debug!("Forwarding action: {:?}", action);
if let Some((_, address)) = self.controllers.get(&uid) {
self.text_to_controller_conn(*address, serde_json::to_string(&action)?);
}
}
WsServerAction::GetRelays(tx) => {
tx.send(self.get_relays()).map_err(|_| io::Error::other("Error sending relays"))?;
}
WsServerAction::ConnectRelay(conn_tx, res_tx) => {
if let Ok(conn_id) = self.connect_relay(conn_tx).await {
if let Err(e) = res_tx.send(conn_id) {
log::error!("Error sending relay conn id: {}", e);
}
}
}
WsServerAction::DisconnectRelayConn(conn) => {
if let Err(err) = self.disconnect_relay_conn(conn).await {
log::error!("Error disconnecting relay conn: {:?}", err);
}
}
}
}
Ok(())
}
pub fn handle_action(
&mut self,
conn: WsConnId,
action: ControllersWsAction,
) {
let action_res = match action {
ControllersWsAction::Register(controller) => self.handle_register(conn, controller),
ControllersWsAction::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);
self.text_to_controller_conn(
conn,
serde_json::to_string(&e).unwrap_or(format!("Error in handling action: {:?}", e)),
);
}
}
pub fn text_to_controller_conn(&self, target: WsConnId, msg: String) {
if let Some((tx, _)) = self.controller_sessions.get(&target) {
log::debug!("Sending message to controller conn {}", target);
// errors if a client has disconnected abruptly and hasn't been timed-out yet
let _ = tx.send(WsControllerAction::Forward(msg.clone()));
}
}
pub fn handle_register(
&mut self,
conn: WsConnId,
controller: Controller,
) -> Result<(), EmgauwaError> {
log::info!(
"Registering controller: {} ({})",
controller.c.name,
controller.c.uid
);
let mut pool_conn = block_on(self.pool.acquire())?;
let c = &controller.c;
let controller_db = block_on(DbController::get_by_uid_or_create(
&mut pool_conn,
&c.uid,
&c.name,
c.relay_count,
))?;
block_on(controller_db.update_active(&mut pool_conn, true))?;
// update only the relay count
block_on(controller_db.update(&mut pool_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(
&mut pool_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(
&mut pool_conn,
schedule.uid.clone(),
&schedule.name,
&schedule.periods,
))?;
relay_schedules.push(new_schedule);
}
block_on(DbJunctionRelaySchedule::set_schedules(
&mut pool_conn,
&new_relay,
relay_schedules.iter().collect(),
))?;
}
}
let relay_states = controller.get_relay_states();
let controller_uid = &controller.c.uid;
let controller_db = block_on(DbController::get_by_uid(&mut pool_conn, controller_uid))?
.ok_or(DatabaseError::InsertGetError)?;
let mut controller = Controller::from_db_model(&mut pool_conn, controller_db)?;
controller.apply_relay_states(&relay_states);
self.controllers.insert(controller_uid.clone(), (controller, conn));
// Update uid in controller session
if let Some((tx, _)) = self.controller_sessions.remove(&conn) {
log::debug!("Updating controller uid for conn {}", conn);
self.controller_sessions.insert(conn, (tx, Some(controller_uid.clone())));
}
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
);
if let Some((controller, _)) = self.controllers.get_mut(&controller_uid) {
controller.apply_relay_states(&relay_states);
}
self.notify_relay_clients()
}
}