Compare commits

...

18 commits
v0.5.0 ... main

Author SHA1 Message Date
066e9f7bf8
Bump version 2024-12-25 20:52:15 +01:00
e923ecb9d8
Add tests and fix is_on calculation 2024-12-25 01:54:16 +01:00
2f5bb538b2
Remove unwrap when getting active schedule 2024-10-28 02:17:38 +01:00
41cc9e0622
Remove logging file from config (was never implemented) 2024-06-11 17:46:35 +02:00
d4ff664f74
Add relay view to faster load controller_uid 2024-06-11 14:10:25 +02:00
277b159200
Add setting to change "midnight" of day 2024-05-30 02:37:37 +02:00
ce7a79d1de
Remove guessing of active_schedule 2024-05-29 15:51:53 +02:00
929985c64a
Improve handling of override_schedule 2024-05-28 21:17:21 +02:00
473832f58a
Rename active_schedule to override_schedule and add EmgauwaNow 2024-05-26 22:48:22 +02:00
9326b66007
Simplify schedule in macro action 2024-05-13 19:17:35 +02:00
f26e66d687
Add parameter for db pool size 2024-05-10 17:43:29 +02:00
b14049b3f6
Add request model for GET tagged schedule 2024-05-06 16:28:10 +02:00
228b366320
Move relay drivers from common to controller 2024-05-05 23:46:49 +02:00
e9b09cd709
Fix minor issues 2024-05-04 18:32:47 +02:00
cacd740bd9
Fix wrong table in macro update 2024-05-02 22:15:19 +02:00
98db89ce03
Fix is_on function for Periods 2024-05-02 20:12:43 +02:00
fc4c1df09a
Revert "Add sql transactions"
This caused the error "locked database".
This reverts commit 19e2ea003b.
2024-05-02 19:35:22 +02:00
19e2ea003b
Add sql transactions 2024-05-02 13:30:47 +02:00
36 changed files with 1434 additions and 1196 deletions

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE macros SET name = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "2b34934e10005378c331f489751dcc4dc5cc79a52299cb74018e36212809288a"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM relays WHERE controller_id = ?",
"query": "SELECT * FROM v_relays WHERE id = ?",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "c9437ff0c3014b269dcb21304fbad12237b9cb69ea6aa4686df6d5262065faa2"
"hash": "2b5ac2227f48be1483f4097da6f890be8091daa97b0af548b6ebf60cdc03dfba"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT relays.* FROM relays INNER JOIN junction_relay_schedule\n\t\t\tON junction_relay_schedule.relay_id = relays.id\n\t\t\tWHERE junction_relay_schedule.schedule_id = ?\n\t\t\tORDER BY junction_relay_schedule.weekday",
"query": "SELECT v_relays.* FROM v_relays INNER JOIN junction_tag ON junction_tag.relay_id = v_relays.id WHERE junction_tag.tag_id = ?",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "2551c285e3e223311cff8e32022d8b11e95d56b2f166326301a0b6722fc1fd44"
"hash": "493ad91be9ce523e9d0f03f5caa9b3255a5426d54901f4f3aa96ad152b05ffd0"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT relay.* FROM relays AS relay INNER JOIN junction_tag ON junction_tag.relay_id = relay.id WHERE junction_tag.tag_id = ?",
"query": "SELECT * FROM v_relays WHERE v_relays.controller_id = ?",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "e94ef5bc8b267d493375bb371dcfb7b09f6355ecbc8b6e1085d5f2f9a08cac3f"
"hash": "4a99db9678cf8d1bdb082c4a13a1f5cdd699bfe7600389e37ca980b6fad12bb5"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM relays",
"query": "SELECT * FROM v_relays",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "ee7da56331bece2efe21b55dbd5f420d3abb08358a1abe301dc7e08693fbef4d"
"hash": "5056b625241d9cbe63d98e00ac39085677c09be8be903804120c2d52579afdbb"
}

View file

@ -1,38 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO relays (name, number, controller_id) VALUES (?, ?, ?) RETURNING *",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "number",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "5865f27b97487b6dfd956a3d260b9bbb0e6c203b721d29cf9149f60bfdd93465"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM relays WHERE controller_id = ? AND number = ?",
"query": "SELECT * FROM v_relays WHERE v_relays.controller_id = ? AND v_relays.number = ?",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "b41855e635ac409559fa63cba4c1285034c573b86e3193da3995606dee412153"
"hash": "9224ad423f2c86f3d95f2b0b7d99a27f690020f89958dfc8dd6044a31afdb31d"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT v_relays.* FROM v_relays INNER JOIN junction_relay_schedule\n\t\t\tON junction_relay_schedule.relay_id = v_relays.id\n\t\t\tWHERE junction_relay_schedule.schedule_id = ?\n\t\t\tORDER BY junction_relay_schedule.weekday",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "number",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "adbce2c94ac0b54d0826b28f99fe63322d3bb1579e52d0f053307e24bd039ef9"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM relays WHERE id = ?",
"query": "SELECT * FROM v_relays WHERE v_relays.id = ?",
"describe": {
"columns": [
{
@ -22,6 +22,11 @@
"name": "controller_id",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "controller_uid",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
false,
false,
false,
false
]
},
"hash": "4f5408e64f5e6a8dd923c3b147f993ce9e4cafc90204b06977481130ec06d111"
"hash": "d57c388bf6c26fe6cadad35d0f254ca2ef93958f9975c585c6de3c437782995d"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO relays (name, number, controller_id) VALUES (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "f85f0a96bb98d20e47677b0679d552812362c3141738b60bc63d673a7f552506"
}

1897
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "emgauwa-common"
version = "0.5.0"
version = "0.5.1"
edition = "2021"
authors = ["Tobias Reisinger <tobias@msrg.cc>"]
@ -8,25 +8,21 @@ authors = ["Tobias Reisinger <tobias@msrg.cc>"]
[dependencies]
actix = "0.13"
actix-web = "4.4"
actix-web-actors = "4.2"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
serde_with = "3.8"
simple_logger = "4.2"
simple_logger = "5.0"
log = "0.4"
config = "0.13"
config = "0.14"
chrono = { version = "0.4", features = ["serde"] }
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros"] }
libsqlite3-sys = { version = "*", features = ["bundled"] }
uuid = "1.6"
uuid = { version = "1.8", features = ["v4"] }
futures = "0.3"
libc = "0.2"
rppal = "0.17"
rppal-pfd = "0.0.5"
rppal-mcp23s17 = "0.0.3"

View file

@ -1,3 +1,5 @@
export DATABASE_URL=sqlite://${PWD}/emgauwa-dev.sqlite
sqlx:
cargo sqlx database drop -y
cargo sqlx database create

View file

@ -0,0 +1 @@
DROP VIEW v_relays;

View file

@ -0,0 +1,8 @@
CREATE VIEW v_relays
AS
SELECT
relays.*,
controllers.uid AS controller_uid
FROM
relays
INNER JOIN controllers ON controllers.id = relays.controller_id;

View file

@ -158,7 +158,7 @@ impl DbController {
) -> Result<Vec<DbRelay>, DatabaseError> {
sqlx::query_as!(
DbRelay,
"SELECT * FROM relays WHERE controller_id = ?",
"SELECT * FROM v_relays WHERE v_relays.controller_id = ?",
self.id
)
.fetch_all(conn.deref_mut())

View file

@ -5,11 +5,10 @@ use sqlx::Sqlite;
use crate::db::{DbRelay, DbSchedule};
use crate::errors::DatabaseError;
use crate::types::Weekday;
pub struct DbJunctionRelaySchedule {
pub id: i64,
pub weekday: Weekday,
pub weekday: i64,
pub relay_id: i64,
pub schedule_id: i64,
}
@ -32,7 +31,7 @@ impl DbJunctionRelaySchedule {
pub async fn get_junction_by_relay_and_weekday(
conn: &mut PoolConnection<Sqlite>,
relay: &DbRelay,
weekday: Weekday,
weekday: i64,
) -> Result<Option<DbJunctionRelaySchedule>, DatabaseError> {
sqlx::query_as!(
DbJunctionRelaySchedule,
@ -51,8 +50,8 @@ impl DbJunctionRelaySchedule {
) -> Result<Vec<DbRelay>, DatabaseError> {
sqlx::query_as!(
DbRelay,
r#"SELECT relays.* FROM relays INNER JOIN junction_relay_schedule
ON junction_relay_schedule.relay_id = relays.id
r#"SELECT v_relays.* FROM v_relays INNER JOIN junction_relay_schedule
ON junction_relay_schedule.relay_id = v_relays.id
WHERE junction_relay_schedule.schedule_id = ?
ORDER BY junction_relay_schedule.weekday"#,
schedule.id
@ -65,7 +64,7 @@ impl DbJunctionRelaySchedule {
pub async fn get_schedule(
conn: &mut PoolConnection<Sqlite>,
relay: &DbRelay,
weekday: Weekday,
weekday: i64,
) -> Result<Option<DbSchedule>, DatabaseError> {
sqlx::query_as!(
DbSchedule,
@ -101,7 +100,7 @@ impl DbJunctionRelaySchedule {
conn: &mut PoolConnection<Sqlite>,
relay: &DbRelay,
schedule: &DbSchedule,
weekday: Weekday,
weekday: i64,
) -> Result<DbJunctionRelaySchedule, DatabaseError> {
match Self::get_junction_by_relay_and_weekday(conn, relay, weekday).await? {
None => sqlx::query_as!(
@ -139,7 +138,7 @@ impl DbJunctionRelaySchedule {
schedules: Vec<&DbSchedule>,
) -> Result<(), DatabaseError> {
for (weekday, schedule) in schedules.iter().enumerate() {
Self::set_schedule(conn, relay, schedule, weekday as Weekday).await?;
Self::set_schedule(conn, relay, schedule, weekday as i64).await?;
}
Ok(())
}

View file

@ -97,7 +97,7 @@ impl DbMacro {
conn: &mut PoolConnection<Sqlite>,
new_name: &str,
) -> Result<DbMacro, DatabaseError> {
sqlx::query!("UPDATE relays SET name = ? WHERE id = ?", new_name, self.id,)
sqlx::query!("UPDATE macros SET name = ? WHERE id = ?", new_name, self.id,)
.execute(conn.deref_mut())
.await?;

View file

@ -23,6 +23,9 @@ pub use relays::DbRelay;
pub use schedules::{DbPeriods, DbSchedule};
pub use tag::DbTag;
#[cfg(test)]
pub(crate) use model_utils::Period;
use crate::errors::{DatabaseError, EmgauwaError};
static MIGRATOR: Migrator = sqlx::migrate!(); // defaults to "./migrations"
@ -33,14 +36,14 @@ pub async fn run_migrations(pool: &Pool<Sqlite>) -> Result<(), EmgauwaError> {
Ok(())
}
pub async fn init(db: &str) -> Result<Pool<Sqlite>, EmgauwaError> {
pub async fn init(db: &str, pool_size: u32) -> Result<Pool<Sqlite>, EmgauwaError> {
let options = SqliteConnectOptions::from_str(db)?
.create_if_missing(true)
.log_statements(log::LevelFilter::Trace);
let pool: Pool<Sqlite> = SqlitePoolOptions::new()
.acquire_timeout(std::time::Duration::from_secs(1))
.max_connections(5)
.max_connections(pool_size)
.connect_with(options)
.await?;

View file

@ -51,13 +51,34 @@ impl Period {
}
}
pub fn is_always_on(&self) -> bool {
self.start.eq(&self.end)
}
pub fn is_on(&self, now: &NaiveTime) -> bool {
self.start.eq(&self.end) || (self.start.le(now) && self.end.gt(now))
if self.is_always_on() {
return true;
}
let start_after_now = self.start.gt(now);
// add check for end time being 00:00 because end being 00:00 would cause end_after_now to always be false
// this will handle end like 24:00 and end_after_now will be true
// same for start_before_end
let end_after_now = self.end.gt(now) || self.end.eq(&NaiveTime::MIN);
let start_before_end = self.start.lt(&self.end) || self.end.eq(&NaiveTime::MIN);
match (start_after_now, end_after_now, start_before_end) {
(false, false, true) => false, // both before now; start before end means "normal" period before now
(false, false, false) => true, // both before now; end before start means "inverse" period around now
(true, false, _) => false, // only start after now
(false, true, _) => true, // only end after now
(true, true, true) => false, // both after now but start first
(true, true, false) => true, // both after now but end first
}
}
pub fn get_next_time(&self, now: &NaiveTime) -> Option<NaiveTime> {
if self.start.eq(&self.end) {
// this period is always on
if self.is_always_on() {
return None;
}

View file

@ -4,10 +4,9 @@ use serde_derive::{Deserialize, Serialize};
use sqlx::pool::PoolConnection;
use sqlx::Sqlite;
use crate::db::{DbController, DbJunctionRelaySchedule, DbJunctionTag, DbSchedule, DbTag};
use crate::db::{DbController, DbJunctionTag, DbTag};
use crate::errors::DatabaseError;
use crate::types::Weekday;
use crate::utils;
use crate::types::EmgauwaUid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbRelay {
@ -15,13 +14,15 @@ pub struct DbRelay {
pub id: i64,
pub name: String,
pub number: i64,
#[serde(rename = "controller_id")]
pub controller_uid: EmgauwaUid,
#[serde(skip)]
pub controller_id: i64,
}
impl DbRelay {
pub async fn get_all(conn: &mut PoolConnection<Sqlite>) -> Result<Vec<DbRelay>, DatabaseError> {
sqlx::query_as!(DbRelay, "SELECT * FROM relays")
sqlx::query_as!(DbRelay, "SELECT * FROM v_relays")
.fetch_all(conn.deref_mut())
.await
.map_err(DatabaseError::from)
@ -31,7 +32,7 @@ impl DbRelay {
conn: &mut PoolConnection<Sqlite>,
id: i64,
) -> Result<Option<DbRelay>, DatabaseError> {
sqlx::query_as!(DbRelay, "SELECT * FROM relays WHERE id = ?", id)
sqlx::query_as!(DbRelay, "SELECT * FROM v_relays WHERE v_relays.id = ?", id)
.fetch_optional(conn.deref_mut())
.await
.map_err(DatabaseError::from)
@ -44,7 +45,7 @@ impl DbRelay {
) -> Result<Option<DbRelay>, DatabaseError> {
sqlx::query_as!(
DbRelay,
"SELECT * FROM relays WHERE controller_id = ? AND number = ?",
"SELECT * FROM v_relays WHERE v_relays.controller_id = ? AND v_relays.number = ?",
controller.id,
number
)
@ -72,7 +73,7 @@ impl DbRelay {
conn: &mut PoolConnection<Sqlite>,
tag: &DbTag,
) -> Result<Vec<DbRelay>, DatabaseError> {
sqlx::query_as!(DbRelay, "SELECT relay.* FROM relays AS relay INNER JOIN junction_tag ON junction_tag.relay_id = relay.id WHERE junction_tag.tag_id = ?", tag.id)
sqlx::query_as!(DbRelay, "SELECT v_relays.* FROM v_relays INNER JOIN junction_tag ON junction_tag.relay_id = v_relays.id WHERE junction_tag.tag_id = ?", tag.id)
.fetch_all(conn.deref_mut())
.await
.map_err(DatabaseError::from)
@ -84,16 +85,25 @@ impl DbRelay {
new_number: i64,
new_controller: &DbController,
) -> Result<DbRelay, DatabaseError> {
sqlx::query_as!(
DbRelay,
"INSERT INTO relays (name, number, controller_id) VALUES (?, ?, ?) RETURNING *",
let result = sqlx::query!(
"INSERT INTO relays (name, number, controller_id) VALUES (?, ?, ?)",
new_name,
new_number,
new_controller.id,
)
.fetch_optional(conn.deref_mut())
.await?
.ok_or(DatabaseError::InsertGetError)
.execute(conn.deref_mut())
.await?;
let last_insert_id = result.last_insert_rowid();
sqlx::query_as!(
DbRelay,
"SELECT * FROM v_relays WHERE id = ?",
last_insert_id
)
.fetch_one(conn.deref_mut())
.await
.map_err(DatabaseError::from)
}
pub async fn delete(&self, conn: &mut PoolConnection<Sqlite>) -> Result<(), DatabaseError> {
@ -163,14 +173,4 @@ impl DbRelay {
.await?
.ok_or(DatabaseError::NotFound)
}
pub async fn get_active_schedule(
&self,
conn: &mut PoolConnection<Sqlite>,
) -> Result<DbSchedule, DatabaseError> {
let weekday = utils::get_weekday();
DbJunctionRelaySchedule::get_schedule(conn, self, weekday as Weekday)
.await?
.ok_or(DatabaseError::NotFound)
}
}

View file

@ -1,35 +0,0 @@
use rppal::gpio::{Gpio, OutputPin};
use crate::drivers::RelayDriver;
use crate::errors::EmgauwaError;
pub struct GpioDriver {
pub gpio: OutputPin,
pub inverted: bool,
}
impl GpioDriver {
pub fn new(pin: u8, inverted: bool) -> Result<Self, EmgauwaError> {
let gpio = Gpio::new()?.get(pin)?.into_output();
Ok(Self { gpio, inverted })
}
}
impl RelayDriver for GpioDriver {
fn set(&mut self, value: bool) -> Result<(), EmgauwaError> {
if self.get_high(value) {
self.gpio.set_high();
} else {
self.gpio.set_low();
}
Ok(())
}
fn get_pin(&self) -> u8 {
self.gpio.pin()
}
fn get_inverted(&self) -> bool {
self.inverted
}
}

View file

@ -1,19 +0,0 @@
mod gpio;
mod null;
mod piface;
pub use gpio::GpioDriver;
pub use null::NullDriver;
pub use piface::PiFaceDriver;
use crate::errors::EmgauwaError;
pub trait RelayDriver {
fn get_high(&self, value: bool) -> bool {
value ^ self.get_inverted()
}
fn set(&mut self, value: bool) -> Result<(), EmgauwaError>;
fn get_pin(&self) -> u8;
fn get_inverted(&self) -> bool;
}

View file

@ -1,26 +0,0 @@
use crate::drivers::RelayDriver;
use crate::errors::EmgauwaError;
pub struct NullDriver {
pub pin: u8,
}
impl NullDriver {
pub fn new(pin: u8) -> Self {
Self { pin }
}
}
impl RelayDriver for NullDriver {
fn set(&mut self, _value: bool) -> Result<(), EmgauwaError> {
Ok(())
}
fn get_pin(&self) -> u8 {
self.pin
}
fn get_inverted(&self) -> bool {
false
}
}

View file

@ -1,52 +0,0 @@
use rppal_pfd::{
ChipSelect, HardwareAddress, OutputPin, PiFaceDigital, PiFaceDigitalError, SpiBus, SpiMode,
};
use crate::drivers::RelayDriver;
use crate::errors::EmgauwaError;
pub struct PiFaceDriver {
pub pfd_pin: OutputPin,
}
impl PiFaceDriver {
pub fn new(pin: u8, pfd: &Option<PiFaceDigital>) -> Result<Self, EmgauwaError> {
let pfd = pfd.as_ref().ok_or(EmgauwaError::Hardware(String::from(
"PiFaceDigital not initialized",
)))?;
let pfd_pin = pfd.get_output_pin(pin)?;
Ok(Self { pfd_pin })
}
pub fn init_piface() -> Result<PiFaceDigital, EmgauwaError> {
let mut pfd = PiFaceDigital::new(
HardwareAddress::new(0)?,
SpiBus::Spi0,
ChipSelect::Cs0,
100_000,
SpiMode::Mode0,
)?;
pfd.init()?;
Ok(pfd)
}
}
impl RelayDriver for PiFaceDriver {
fn set(&mut self, value: bool) -> Result<(), EmgauwaError> {
if self.get_high(value) {
self.pfd_pin.set_high().map_err(PiFaceDigitalError::from)?;
} else {
self.pfd_pin.set_low().map_err(PiFaceDigitalError::from)?;
}
Ok(())
}
fn get_pin(&self) -> u8 {
self.pfd_pin.get_pin_number()
}
fn get_inverted(&self) -> bool {
false
}
}

View file

@ -6,9 +6,6 @@ use actix::MailboxError;
use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use config::ConfigError;
use rppal::gpio;
use rppal_mcp23s17::Mcp23s17Error;
use rppal_pfd::PiFaceDigitalError;
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
@ -50,7 +47,9 @@ impl From<&EmgauwaError> for String {
EmgauwaError::Database(err) => String::from(err),
EmgauwaError::Uid(_) => String::from("the uid is in a bad format"),
EmgauwaError::Internal(_) => String::from("internal error"),
EmgauwaError::Connection(_) => String::from("the target controller is not connected"),
EmgauwaError::Connection(uid) => {
format!("unable to connect to controller with uid: {}", uid)
}
EmgauwaError::Other(err) => format!("other error: {}", err),
EmgauwaError::Hardware(err) => format!("hardware error: {}", err),
}
@ -99,25 +98,6 @@ impl From<ConfigError> for EmgauwaError {
}
}
impl From<gpio::Error> for EmgauwaError {
fn from(value: gpio::Error) -> Self {
Self::Hardware(value.to_string())
}
}
impl From<PiFaceDigitalError> for EmgauwaError {
fn from(value: PiFaceDigitalError) -> Self {
match value {
PiFaceDigitalError::Mcp23s17Error { source } => match source {
Mcp23s17Error::SpiError { source } => Self::Hardware(source.to_string()),
_ => Self::Hardware(source.to_string()),
},
PiFaceDigitalError::GpioError { source } => Self::Hardware(source.to_string()),
_ => Self::Hardware(value.to_string()),
}
}
}
impl From<&EmgauwaError> for HttpResponse {
fn from(err: &EmgauwaError) -> Self {
HttpResponse::build(err.get_code()).json(err)
@ -139,7 +119,7 @@ impl Serialize for EmgauwaError {
{
let mut s = serializer.serialize_struct("error", 2)?;
s.serialize_field("code", &self.get_code().as_u16())?;
s.serialize_field("description", &String::from(self))?;
s.serialize_field("message", &String::from(self))?;
s.end()
}
}

View file

@ -1,8 +1,72 @@
pub mod constants;
pub mod db;
pub mod drivers;
pub mod errors;
pub mod models;
pub mod settings;
pub mod types;
pub mod utils;
#[cfg(test)]
mod periods {
use chrono::NaiveTime;
use crate::db::Period;
use crate::types::EmgauwaNow;
const MIDNIGHT: NaiveTime = NaiveTime::MIN;
fn new_time(hour: u32, minute: u32) -> NaiveTime {
NaiveTime::from_hms_opt(hour, minute, 0).expect("Failed to create NaiveTime")
}
fn new_period(start_hour: u32, start_minute: u32, end_hour: u32, end_minute: u32) -> Period {
Period {
start: new_time(start_hour, start_minute),
end: new_time(end_hour, end_minute),
}
}
#[test]
fn always_on() {
let period = Period::new_on();
let now: EmgauwaNow = EmgauwaNow::now(&MIDNIGHT);
assert_eq!(period.is_always_on(), true);
assert_eq!(period.is_on(&MIDNIGHT), true);
assert_eq!(period.is_on(&new_time(12, 00)), true);
assert_eq!(period.is_on(&now.time), true);
}
#[test]
fn simple_period() {
let period = new_period(11, 00, 13, 00);
assert_eq!(period.is_always_on(), false);
assert_eq!(period.is_on(&MIDNIGHT), false);
assert_eq!(period.is_on(&new_time(10, 00)), false);
assert_eq!(period.is_on(&new_time(11, 00)), true);
assert_eq!(period.is_on(&new_time(12, 00)), true);
assert_eq!(period.is_on(&new_time(13, 00)), false);
assert_eq!(period.is_on(&new_time(14, 00)), false);
}
#[test]
fn to_midnight_period() {
let period = new_period(22, 00, 00, 00);
assert_eq!(period.is_always_on(), false);
assert_eq!(period.is_on(&MIDNIGHT), false);
assert_eq!(period.is_on(&new_time(21, 00)), false);
assert_eq!(period.is_on(&new_time(22, 00)), true);
assert_eq!(period.is_on(&new_time(23, 00)), true);
assert_eq!(period.is_on(&new_time(00, 00)), false);
assert_eq!(period.is_on(&new_time(01, 00)), false);
}
#[test]
fn from_midnight_period() {
let period = new_period(00, 00, 02, 00);
assert_eq!(period.is_always_on(), false);
assert_eq!(period.is_on(&MIDNIGHT), true);
assert_eq!(period.is_on(&new_time(23, 00)), false);
assert_eq!(period.is_on(&new_time(00, 00)), true);
assert_eq!(period.is_on(&new_time(01, 00)), true);
assert_eq!(period.is_on(&new_time(02, 00)), false);
assert_eq!(period.is_on(&new_time(03, 00)), false);
}
}

View file

@ -9,8 +9,8 @@ use sqlx::Sqlite;
use crate::db::DbController;
use crate::errors::{DatabaseError, EmgauwaError};
use crate::models::{convert_db_list_cache, FromDbModel, Relay};
use crate::types::RelayStates;
use crate::models::{convert_db_list, FromDbModel, Relay};
use crate::types::{EmgauwaNow, RelayState, RelayStates};
#[derive(Serialize, Deserialize, Debug, Clone, MessageResponse)]
pub struct Controller {
@ -28,7 +28,7 @@ impl FromDbModel for Controller {
db_model: Self::DbModel,
) -> Result<Self, DatabaseError> {
let relays_db = block_on(db_model.get_relays(conn))?;
let cache = convert_db_list_cache(conn, relays_db, db_model.clone())?;
let cache = convert_db_list(conn, relays_db)?;
Self::from_db_model_cache(conn, db_model, cache)
}
@ -57,19 +57,20 @@ impl Controller {
self.relays
.iter_mut()
.zip(relay_states.iter())
.for_each(|(relay, is_on)| {
relay.is_on = *is_on;
});
.for_each(|(relay, state)| relay.apply_state(state));
}
pub fn get_relay_states(&self) -> RelayStates {
self.relays.iter().map(|r| r.is_on).collect()
self.relays.iter().map(RelayState::from).collect()
}
pub fn get_next_time(&self, now: &NaiveTime) -> Option<NaiveTime> {
pub fn check_next_time(&mut self, now: &EmgauwaNow) -> Option<NaiveTime> {
self.relays
.iter()
.filter_map(|r| r.active_schedule.get_next_time(now))
.iter_mut()
.filter_map(|r| {
r.reload_active_schedule(now.weekday);
r.get_next_time(&now.time)
})
.min()
}
@ -80,6 +81,8 @@ impl Controller {
.find(|r| r.r.number == relay_num)
.ok_or(EmgauwaError::Other(String::from("Relay not found")))?;
log::debug!("Pulsing relay {} until {:?}", relay_num, until);
relay.pulsing = Some(until);
Ok(())
}

View file

@ -3,13 +3,13 @@ use serde_derive::{Deserialize, Serialize};
use sqlx::pool::PoolConnection;
use sqlx::Sqlite;
use crate::db::{DbJunctionRelaySchedule, DbMacroAction};
use crate::db::{DbJunctionRelaySchedule, DbMacroAction, DbSchedule};
use crate::errors::{DatabaseError, EmgauwaError};
use crate::models::{FromDbModel, Relay, Schedule};
use crate::models::{FromDbModel, Relay};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MacroAction {
pub schedule: Schedule,
pub schedule: DbSchedule,
pub relay: Relay,
pub weekday: i64,
}
@ -30,8 +30,7 @@ impl FromDbModel for MacroAction {
db_model: Self::DbModel,
_cache: Self::DbModelCache,
) -> Result<Self, DatabaseError> {
let schedule_db = block_on(db_model.get_schedule(conn))?;
let schedule = Schedule::from_db_model(conn, schedule_db)?;
let schedule = block_on(db_model.get_schedule(conn))?;
let relay_db = block_on(db_model.get_relay(conn))?;
let relay = Relay::from_db_model(conn, relay_db)?;
@ -48,7 +47,7 @@ impl FromDbModel for MacroAction {
impl MacroAction {
pub async fn execute(&self, conn: &mut PoolConnection<Sqlite>) -> Result<(), EmgauwaError> {
DbJunctionRelaySchedule::set_schedule(conn, &self.relay.r, &self.schedule.s, self.weekday)
DbJunctionRelaySchedule::set_schedule(conn, &self.relay.r, &self.schedule, self.weekday)
.await?;
Ok(())
}

View file

@ -1,66 +1,65 @@
use std::time::Instant;
use chrono::NaiveTime;
use chrono::{NaiveTime, Weekday};
use futures::executor::block_on;
use serde_derive::{Deserialize, Serialize};
use sqlx::pool::PoolConnection;
use sqlx::Sqlite;
use crate::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
use crate::db::{DbJunctionRelaySchedule, DbRelay, DbSchedule};
use crate::errors::DatabaseError;
use crate::models::FromDbModel;
use crate::types::EmgauwaUid;
use crate::types::RelayState;
use crate::utils;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Relay {
#[serde(flatten)]
pub r: DbRelay,
pub controller: DbController,
pub controller_id: EmgauwaUid,
pub schedules: Vec<DbSchedule>,
pub active_schedule: DbSchedule,
pub active_schedule: Option<DbSchedule>,
pub override_schedule: Option<DbSchedule>,
pub is_on: Option<bool>,
pub tags: Vec<String>,
// for internal use only.
#[serde(skip)]
pub pulsing: Option<Instant>,
#[serde(skip, default = "utils::default_weekday")]
pub override_schedule_weekday: Weekday,
}
impl FromDbModel for Relay {
type DbModel = DbRelay;
type DbModelCache = DbController;
type DbModelCache = ();
fn from_db_model(
conn: &mut PoolConnection<Sqlite>,
db_model: Self::DbModel,
) -> Result<Self, DatabaseError> {
let cache = block_on(db_model.get_controller(conn))?;
Self::from_db_model_cache(conn, db_model, cache)
Self::from_db_model_cache(conn, db_model, ())
}
fn from_db_model_cache(
conn: &mut PoolConnection<Sqlite>,
db_model: Self::DbModel,
cache: Self::DbModelCache,
_cache: Self::DbModelCache,
) -> Result<Self, DatabaseError> {
let tags = block_on(db_model.get_tags(conn))?;
let controller_id = cache.uid.clone();
let schedules = block_on(DbJunctionRelaySchedule::get_schedules(conn, &db_model))?;
let active_schedule = block_on(db_model.get_active_schedule(conn))?;
let is_on = None;
Ok(Relay {
r: db_model,
controller: cache,
controller_id,
schedules,
active_schedule,
active_schedule: None,
override_schedule: None,
is_on,
tags,
pulsing: None,
override_schedule_weekday: utils::default_weekday(),
})
}
}
@ -69,25 +68,24 @@ impl Relay {
pub fn reload(&mut self, conn: &mut PoolConnection<Sqlite>) -> Result<(), DatabaseError> {
self.r = block_on(self.r.reload(conn))?;
self.schedules = block_on(DbJunctionRelaySchedule::get_schedules(conn, &self.r))?;
self.reload_active_schedule(conn)?;
Ok(())
}
pub fn reload_active_schedule(
&mut self,
conn: &mut PoolConnection<Sqlite>,
) -> Result<(), DatabaseError> {
self.active_schedule = block_on(self.r.get_active_schedule(conn))?;
Ok(())
}
pub fn is_on(&self, now: &NaiveTime) -> bool {
self.active_schedule.is_on(now)
if let Some(active_schedule) = &self.active_schedule {
active_schedule.is_on(now)
} else {
false
}
}
pub fn get_next_time(&self, now: &NaiveTime) -> Option<NaiveTime> {
self.active_schedule.get_next_time(now)
if let Some(active_schedule) = &self.active_schedule {
active_schedule.get_next_time(now)
} else {
None
}
}
pub fn check_pulsing(&mut self, now: &Instant) -> Option<Instant> {
@ -103,4 +101,32 @@ impl Relay {
None => None,
}
}
pub fn reload_active_schedule(&mut self, weekday: Weekday) {
if let Some(schedule) = &self.override_schedule {
if self.override_schedule_weekday == weekday {
self.active_schedule = Some(schedule.clone());
return;
}
if self.override_schedule_weekday != weekday {
self.override_schedule = None;
}
}
if let Some(schedule) = self.schedules.get(weekday as usize) {
self.active_schedule = Some(schedule.clone());
}
}
pub fn apply_state(&mut self, state: &RelayState) {
self.active_schedule.clone_from(&state.active_schedule);
self.override_schedule.clone_from(&state.override_schedule);
self.is_on = state.is_on;
}
pub fn find_and_apply_state(&mut self, stated_relays: &[Relay]) {
if let Some(stated_relay) = stated_relays.iter().find(|r| r.r.id == self.r.id) {
self.apply_state(&stated_relay.into());
}
}
}

View file

@ -16,7 +16,6 @@ pub struct Server {
#[allow(unused)]
pub struct Logging {
pub level: String,
pub file: String,
}
#[derive(Clone, Debug, Deserialize, Default)]
@ -40,7 +39,6 @@ impl Default for Logging {
fn default() -> Self {
Logging {
level: String::from("info"),
file: String::from("stdout"),
}
}
}

27
src/types/emgauwa_now.rs Normal file
View file

@ -0,0 +1,27 @@
use std::time::Instant;
use chrono::{Local, NaiveTime, Timelike, Weekday};
use crate::utils;
pub struct EmgauwaNow {
pub time: NaiveTime,
pub instant: Instant,
pub weekday: Weekday,
pub midnight: NaiveTime,
}
impl EmgauwaNow {
pub fn now(midnight: &NaiveTime) -> EmgauwaNow {
EmgauwaNow {
time: Local::now().time(),
instant: Instant::now(),
weekday: utils::get_weekday(midnight),
midnight: *midnight,
}
}
pub fn num_seconds_from_midnight(&self) -> u32 {
self.time.num_seconds_from_midnight()
}
}

View file

@ -1,9 +1,13 @@
mod emgauwa_now;
mod emgauwa_uid;
mod request;
mod schedule_uid;
mod relay_state;
use actix::Message;
pub use emgauwa_now::EmgauwaNow;
pub use emgauwa_uid::EmgauwaUid;
pub use relay_state::{RelayState, RelayStates};
pub use request::*;
pub use schedule_uid::ScheduleUid;
use serde_derive::{Deserialize, Serialize};
@ -12,10 +16,6 @@ use crate::db::DbSchedule;
use crate::errors::EmgauwaError;
use crate::models::{Controller, Relay};
pub type Weekday = i64;
pub type RelayStates = Vec<Option<bool>>;
#[derive(Debug, Serialize, Deserialize, Message)]
#[rtype(result = "Result<(), EmgauwaError>")]
pub enum ControllerWsAction {

22
src/types/relay_state.rs Normal file
View file

@ -0,0 +1,22 @@
use serde_derive::{Deserialize, Serialize};
use crate::db::DbSchedule;
use crate::models::Relay;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelayState {
pub active_schedule: Option<DbSchedule>,
pub override_schedule: Option<DbSchedule>,
pub is_on: Option<bool>
}
pub type RelayStates = Vec<RelayState>;
impl From<&Relay> for RelayState {
fn from(relay: &Relay) -> Self {
RelayState {
active_schedule: relay.active_schedule.clone(),
override_schedule: relay.override_schedule.clone(),
is_on: relay.is_on
}
}
}

View file

@ -23,11 +23,21 @@ pub struct RequestScheduleUpdate {
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestRelayUpdate {
pub name: Option<String>,
pub active_schedule: Option<RequestScheduleId>,
#[serde(
default, // <- important for deserialization
skip_serializing_if = "Option::is_none", // <- important for serialization
with = "::serde_with::rust::double_option",
)]
pub override_schedule: Option<Option<RequestScheduleId>>,
pub schedules: Option<Vec<RequestScheduleId>>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct RequestScheduleGetTagged {
pub strict: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestRelayPulse {
pub duration: Option<u32>,

View file

@ -2,17 +2,17 @@ use std::ffi::CString;
use std::io::{Error, ErrorKind};
use std::str::FromStr;
use chrono::Datelike;
use chrono::{Datelike, NaiveTime, Weekday};
use log::LevelFilter;
use simple_logger::SimpleLogger;
use crate::errors::EmgauwaError;
use crate::settings::Permissions;
use crate::types::{RelayStates, Weekday};
use crate::settings::{Logging, Permissions};
use crate::types::RelayStates;
pub fn init_logging(level: &str) -> Result<(), EmgauwaError> {
let log_level: LevelFilter = LevelFilter::from_str(level)
.map_err(|_| EmgauwaError::Other(format!("Invalid log level: {}", level)))?;
pub fn init_logging(logging: &Logging) -> Result<(), EmgauwaError> {
let log_level: LevelFilter = LevelFilter::from_str(&logging.level)
.map_err(|_| EmgauwaError::Other(format!("Invalid log level: {}", logging.level)))?;
log::trace!("Log level set to {:?}", log_level);
SimpleLogger::new()
@ -92,18 +92,24 @@ fn drop_privileges_user(user: &str) -> Result<(), Error> {
Ok(())
}
pub fn get_weekday() -> Weekday {
(chrono::offset::Local::now()
.date_naive()
.weekday()
.number_from_monday()
- 1) as Weekday
pub fn get_weekday(midnight: &NaiveTime) -> Weekday {
let dt = chrono::offset::Local::now().naive_local();
let weekday = dt.weekday();
if dt.time().lt(midnight) {
weekday.pred()
} else {
weekday
}
}
pub fn default_weekday() -> Weekday {
Weekday::Mon
}
pub fn printable_relay_states(relay_states: &RelayStates) -> String {
let mut relay_debug = String::new();
relay_states.iter().for_each(|state| {
relay_debug.push_str(match state {
relay_debug.push_str(match state.is_on {
Some(true) => "+",
Some(false) => "-",
None => "?",