Compare commits
18 Commits
e135a34d4c
...
f84c8226a7
| Author | SHA1 | Date | |
|---|---|---|---|
| f84c8226a7 | |||
| 024a212fe2 | |||
| 5a818bae56 | |||
| a4457dc2d7 | |||
| d1d7f89dfb | |||
| a65f9852f3 | |||
| 76067d76ac | |||
| 37b8fe5e56 | |||
| cf67c980a2 | |||
| 0ad039a597 | |||
| 3db69454e4 | |||
| a7947a31bc | |||
| d8416f3c99 | |||
| 8ed9d872dc | |||
| a86a28fd85 | |||
| 05ba925d54 | |||
| f7e55536ab | |||
| a398911527 |
1910
Cargo.lock
generated
1910
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -10,7 +10,11 @@ chrono = { version = "0.4.42", features = ["serde"] }
|
|||||||
axum = { version = "0.8.8", features = ["macros"] }
|
axum = { version = "0.8.8", features = ["macros"] }
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
actix-web = "4.12.1"
|
sea-orm = { version = "1.1.19", features = [
|
||||||
paperclip = { version = "0.9.5", features = ["actix4"] }
|
"macros",
|
||||||
apistos = { version = "0.6.0", features = ["swagger-ui"] }
|
"runtime-tokio",
|
||||||
schemars = { package = "apistos-schemars", version = "0.8" }
|
"sqlx-sqlite",
|
||||||
|
] }
|
||||||
|
utoipa = { version = "5.4.0", features = ["axum_extras", "chrono"] }
|
||||||
|
utoipa-axum = "0.2.0"
|
||||||
|
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
|
||||||
|
|||||||
32
flake.lock
generated
32
flake.lock
generated
@@ -6,11 +6,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763880175,
|
"lastModified": 1770275419,
|
||||||
"narHash": "sha256-WfItZn6duisxCxyltbu7Hs7kxzNeylgZGOwCYwHe26g=",
|
"narHash": "sha256-g2wfAevB/IFF6Y1C74TbhRERlUVFVGQsgGp/lLR4lQM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "a563f057979806c59da53070297502eb7af22f62",
|
"rev": "aa3fbaab2bdc73c5c1e25a124c272dde295bc957",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763384566,
|
"lastModified": 1769799857,
|
||||||
"narHash": "sha256-r+wgI+WvNaSdxQmqaM58lVNvJYJ16zoq+tKN20cLst4=",
|
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "d4155d6ebb70fbe2314959842f744aa7cabbbf6a",
|
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763678758,
|
"lastModified": 1770197578,
|
||||||
"narHash": "sha256-+hBiJ+kG5IoffUOdlANKFflTT5nO3FrrR2CA3178Y5s=",
|
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "117cc7f94e8072499b0a7aa4c52084fa4e11cc9b",
|
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -95,16 +95,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1688392541,
|
"lastModified": 1770197578,
|
||||||
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
|
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
|
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-22.11",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
@@ -119,11 +119,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763846202,
|
"lastModified": 1770200365,
|
||||||
"narHash": "sha256-f5PvQONttEQCjnQ52zAEVJvXDZ5l2gbItLfDyfcyGgk=",
|
"narHash": "sha256-Z3V5v8tSwZ3l4COVSt0b6Av0wZwTUf7Qj0SQ2/Z5RX0=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "50621856a594a357c3aff0c5176ba8db4118133d",
|
"rev": "1433910d1ffaff2c7a5fb7ba701f82ea578a99e3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
29
flake.nix
29
flake.nix
@@ -2,7 +2,7 @@
|
|||||||
inputs = {
|
inputs = {
|
||||||
fenix.url = "github:nix-community/fenix";
|
fenix.url = "github:nix-community/fenix";
|
||||||
naersk.url = "github:nix-community/naersk/master";
|
naersk.url = "github:nix-community/naersk/master";
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
@@ -101,23 +101,28 @@
|
|||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
shell = eachSystem (buildTargets) (
|
devShells = eachSystem (builtins.attrNames buildTargets) (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = mkPkgs system null;
|
pkgs = mkPkgs system null;
|
||||||
in
|
in
|
||||||
pkgs.mkShell {
|
{
|
||||||
buildInputs = with pkgs; [
|
default = pkgs.mkShell {
|
||||||
gcc
|
name = "snooze-pal";
|
||||||
openssl.dev
|
buildInputs = with pkgs; [
|
||||||
pkg-config
|
gcc
|
||||||
libiconv
|
openssl.dev
|
||||||
rustc
|
pkg-config
|
||||||
cargo
|
libiconv
|
||||||
sea-orm-cli
|
rustc
|
||||||
];
|
cargo
|
||||||
|
sea-orm-cli
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
packages = eachCrossSystem (builtins.attrNames buildTargets) (
|
packages = eachCrossSystem (builtins.attrNames buildTargets) (
|
||||||
buildSystem: targetSystem:
|
buildSystem: targetSystem:
|
||||||
let
|
let
|
||||||
|
|||||||
2
migration/.gitignore
vendored
Normal file
2
migration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
result
|
||||||
2129
migration/Cargo.lock
generated
Normal file
2129
migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
migration/Cargo.toml
Normal file
23
migration/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "migration"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "migration"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = {version = "1.49.0", features = ["macros", "rt-multi-thread"]}
|
||||||
|
|
||||||
|
[dependencies.sea-orm-migration]
|
||||||
|
version = "1.1.0"
|
||||||
|
features = [
|
||||||
|
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||||
|
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||||
|
# e.g.
|
||||||
|
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||||
|
# "sqlx-postgres", # `DATABASE_DRIVER` feature
|
||||||
|
"sqlx-sqlite"
|
||||||
|
]
|
||||||
41
migration/README.md
Normal file
41
migration/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Running Migrator CLI
|
||||||
|
|
||||||
|
- Generate a new migration file
|
||||||
|
```sh
|
||||||
|
cargo run -- generate MIGRATION_NAME
|
||||||
|
```
|
||||||
|
- Apply all pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
cargo run -- up
|
||||||
|
```
|
||||||
|
- Apply first 10 pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- up -n 10
|
||||||
|
```
|
||||||
|
- Rollback last applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down
|
||||||
|
```
|
||||||
|
- Rollback last 10 applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down -n 10
|
||||||
|
```
|
||||||
|
- Drop all tables from the database, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- fresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- refresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- reset
|
||||||
|
```
|
||||||
|
- Check the status of all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- status
|
||||||
|
```
|
||||||
12
migration/src/lib.rs
Normal file
12
migration/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
mod m20220101_000001_create_table;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![Box::new(m20220101_000001_create_table::Migration)]
|
||||||
|
}
|
||||||
|
}
|
||||||
36
migration/src/m20220101_000001_create_table.rs
Normal file
36
migration/src/m20220101_000001_create_table.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Replace the sample below with your own migration scripts
|
||||||
|
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alarm::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_auto(Alarm::Id))
|
||||||
|
.col(date_time(Alarm::Time))
|
||||||
|
.col(boolean(Alarm::Enabled))
|
||||||
|
.col(string(Alarm::Title))
|
||||||
|
.to_owned(),
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Replace the sample below with your own migration scripts
|
||||||
|
manager.drop_table(Table::drop().table(Alarm::Table).to_owned()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Alarm {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Time,
|
||||||
|
Enabled,
|
||||||
|
Title
|
||||||
|
}
|
||||||
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run_cli(migration::Migrator).await;
|
||||||
|
}
|
||||||
BIN
snooze-pal.db
Normal file
BIN
snooze-pal.db
Normal file
Binary file not shown.
45
src/dao/alarm.rs
Normal file
45
src/dao/alarm.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::i32;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::model::{self, alarm};
|
||||||
|
|
||||||
|
pub async fn create_alarm<C: ConnectionTrait>(
|
||||||
|
db: &C,
|
||||||
|
title: String,
|
||||||
|
time: DateTime<Utc>,
|
||||||
|
) -> Result<alarm::Model, DbErr> {
|
||||||
|
let alarm_to_create = model::alarm::ActiveModel {
|
||||||
|
title: Set(title),
|
||||||
|
time: Set(time.naive_utc()),
|
||||||
|
enabled: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(alarm_to_create.insert(db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_alarms<C: ConnectionTrait>(db: &C, enabled: Option<bool>) -> Result<Vec<alarm::Model>, DbErr> {
|
||||||
|
let query = model::alarm::Entity::find();
|
||||||
|
|
||||||
|
let query = match enabled {
|
||||||
|
Some(enabled) => {
|
||||||
|
query.filter(model::alarm::Column::Enabled.eq(enabled))
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
query
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
query.all(db).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_alarm<C: ConnectionTrait>(db: &C, id: i32) -> Result<Option<model::alarm::Model>, DbErr> {
|
||||||
|
let result = model::alarm::Entity::find_by_id(id)
|
||||||
|
.one(db).await;
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
2
src/dao/mod.rs
Normal file
2
src/dao/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod alarm;
|
||||||
|
|
||||||
77
src/main.rs
77
src/main.rs
@@ -1,64 +1,81 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use actix_web::{App, HttpServer, web};
|
|
||||||
use apistos::{SwaggerUIConfig, app::{BuildConfig, OpenApiWrapper}, info::Info, spec::Spec};
|
|
||||||
use cron_tab::Cron;
|
use cron_tab::Cron;
|
||||||
use gpio_cdev::Chip;
|
use gpio_cdev::Chip;
|
||||||
|
use sea_orm::Database;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_axum::router::OpenApiRouter;
|
||||||
|
use utoipa_scalar::{Scalar, Servable};
|
||||||
|
|
||||||
use crate::{ringer::BeepRinger, scheduler::Scheduler};
|
use crate::{ringer::{BeepRinger, Ringer, SilentRinger}, scheduler::Scheduler};
|
||||||
|
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod ringer;
|
mod ringer;
|
||||||
mod types;
|
mod types;
|
||||||
mod resources;
|
mod resources;
|
||||||
|
mod dao;
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
scheduler: Arc<Scheduler<BeepRinger>>
|
scheduler: Arc<Scheduler>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app_state() -> AppState {
|
fn construct_ringer() -> Arc<Mutex<dyn Ringer>> {
|
||||||
let chip: Arc<Mutex<Chip>> = Arc::new(Mutex::new(Chip::new("/dev/gpiochip0").unwrap()));
|
let result = Chip::new("/dev/gpiochip0");
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(chip) => {
|
||||||
|
let chip = Arc::new(Mutex::new(chip));
|
||||||
|
Arc::new(Mutex::new(BeepRinger::new(chip))) as Arc<Mutex<dyn Ringer>>
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error opening chip (falling back to silent ringer): {}", e);
|
||||||
|
Arc::new(Mutex::new(SilentRinger::new())) as Arc<Mutex<dyn Ringer>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn app_state() -> AppState {
|
||||||
let cron = Arc::new(Mutex::new(Cron::new(chrono::Local)));
|
let cron = Arc::new(Mutex::new(Cron::new(chrono::Local)));
|
||||||
|
let db = Database::connect("sqlite://snooze-pal.db").await.unwrap();
|
||||||
|
let ringer = construct_ringer();
|
||||||
|
|
||||||
AppState {
|
AppState {
|
||||||
scheduler: Arc::new(Scheduler::new(
|
scheduler: Arc::new(Scheduler::new(
|
||||||
Arc::new(Mutex::new(BeepRinger::new(chip.clone()))),
|
ringer,
|
||||||
cron.clone()
|
cron.clone(),
|
||||||
|
Arc::new(db)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let app_state = app_state();
|
let app_state = app_state().await;
|
||||||
|
app_state.scheduler.start().await;
|
||||||
app_state.scheduler.start();
|
start_axum_server(app_state).await;
|
||||||
|
|
||||||
let _ = start_actix_server(app_state).await;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
struct ApiDocs;
|
||||||
|
|
||||||
async fn start_actix_server(app_state: AppState) -> std::io::Result<()> {
|
async fn start_axum_server(app_state: AppState) {
|
||||||
let _ = HttpServer::new(move || {
|
let docs = ApiDocs::openapi();
|
||||||
let spec = Spec {
|
|
||||||
info: Info {
|
|
||||||
title: "Snooze Pal".to_string(),
|
|
||||||
version: "0.0.1".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
App::new()
|
let (router, spec) = OpenApiRouter::<AppState>::with_openapi(docs)
|
||||||
.document(spec)
|
.nest("/v1", resources::router())
|
||||||
.service(resources::v1())
|
.with_state(app_state)
|
||||||
.app_data(web::Data::new(app_state.clone()))
|
.split_for_parts();
|
||||||
.build_with("/openapi.json", BuildConfig::default().with(SwaggerUIConfig::new(&"/swagger")))
|
|
||||||
|
|
||||||
}).bind("0.0.0.0:8080")?.run().await;
|
let router = router.merge(Scalar::with_url("/docs", spec));
|
||||||
|
let listener = TcpListener::bind("0.0.0.0:8080")
|
||||||
|
.await.expect("Failed to bind to port 8080. It may be taken by another process.");
|
||||||
|
|
||||||
Ok(())
|
axum::serve(listener, router)
|
||||||
|
.await.expect("Failed to serve");
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/model/alarm.rs
Normal file
18
src/model/alarm.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "alarm")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub time: DateTime,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
5
src/model/mod.rs
Normal file
5
src/model/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod alarm;
|
||||||
1
src/model/prelude.rs
Normal file
1
src/model/prelude.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
65
src/resources/alarm/get.rs
Normal file
65
src/resources/alarm/get.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use std::i32;
|
||||||
|
|
||||||
|
use axum::{Json, debug_handler, extract::{Query, State}, http::StatusCode, response::IntoResponse};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, IntoResponses, ToSchema};
|
||||||
|
|
||||||
|
use crate::{AppState, types::Alarm};
|
||||||
|
|
||||||
|
#[derive(ToSchema, Serialize)]
|
||||||
|
pub struct OkResponse {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
enabled: bool,
|
||||||
|
time: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoResponses)]
|
||||||
|
pub enum Responses {
|
||||||
|
#[response(status = 200)]
|
||||||
|
Ok(#[to_schema] Vec<OkResponse>),
|
||||||
|
#[response(status = 500)]
|
||||||
|
DBError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Responses {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||||
|
Responses::DBError(message) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoParams, Deserialize)]
|
||||||
|
pub struct RequestQuery {
|
||||||
|
enabled: Option<bool>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Alarm> for OkResponse {
|
||||||
|
fn from(value: Alarm) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
title: value.title,
|
||||||
|
enabled: value.enabled,
|
||||||
|
time: value.time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(get, path = "", responses(Responses), params(RequestQuery))]
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn get_handler(State(AppState{ scheduler }): State<AppState>, Query(RequestQuery { enabled }): Query<RequestQuery>) -> Responses {
|
||||||
|
let result = scheduler.get_alarms(enabled).await;
|
||||||
|
match result {
|
||||||
|
Ok(alarms) => {
|
||||||
|
Responses::Ok(alarms.into_iter().map(|alarm| alarm.into()).collect())
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
Responses::DBError(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
|
mod get;
|
||||||
|
mod patch;
|
||||||
mod post;
|
mod post;
|
||||||
use apistos::web;
|
|
||||||
|
|
||||||
pub fn resource() -> web::Resource {
|
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||||
web::resource("/alarm")
|
|
||||||
.route(web::post().to(post::post))
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub fn router() -> OpenApiRouter<AppState> {
|
||||||
|
OpenApiRouter::new().routes(routes!(
|
||||||
|
post::post_handler,
|
||||||
|
get::get_handler,
|
||||||
|
patch::patch_handler
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/resources/alarm/patch.rs
Normal file
73
src/resources/alarm/patch.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::{debug_handler, response::IntoResponse};
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoResponses, ToSchema};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(ToSchema, Serialize, Deserialize)]
|
||||||
|
pub struct OkResponse {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
enabled: bool,
|
||||||
|
time: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ToSchema, Deserialize)]
|
||||||
|
pub struct PatchRequestBody {
|
||||||
|
title: Option<String>,
|
||||||
|
enabled: Option<bool>,
|
||||||
|
time: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoResponses)]
|
||||||
|
pub enum Responses {
|
||||||
|
#[response(status = 200)]
|
||||||
|
Ok(#[to_schema] OkResponse),
|
||||||
|
#[response(status = 500, description = "Something failed in the Database when trying to update the alarm")]
|
||||||
|
DbError(String),
|
||||||
|
#[response(status = 404, description = "The alarm you want to update was not found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Responses {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||||
|
Responses::DbError(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
|
||||||
|
Responses::NotFound => (StatusCode::NOT_FOUND).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::types::Alarm> for OkResponse {
|
||||||
|
fn from(value: crate::types::Alarm) -> Self {
|
||||||
|
OkResponse {
|
||||||
|
id: value.id,
|
||||||
|
title: value.title,
|
||||||
|
enabled: value.enabled,
|
||||||
|
time: value.time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(patch, path = "/{id}", responses(Responses))]
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn patch_handler(State(AppState { scheduler }): State<AppState>, Path(id): Path<i32>, Json(body): Json<PatchRequestBody>) -> Responses {
|
||||||
|
let result = scheduler.update_alarm(id, body.title, body.enabled, body.time).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Some(alarm)) => {
|
||||||
|
Responses::Ok(alarm.into())
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
Responses::NotFound
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
Responses::DbError(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +1,75 @@
|
|||||||
use std::{fmt::Display};
|
use axum::extract::State;
|
||||||
|
use axum::{Json};
|
||||||
use actix_web::{ResponseError, web::{Data, Json}};
|
use axum::http::StatusCode;
|
||||||
use apistos::{ApiComponent, ApiErrorComponent, actix::CreatedJson};
|
use axum::debug_handler;
|
||||||
use chrono::{DateTime, Local};
|
use axum::response::IntoResponse;
|
||||||
use schemars::JsonSchema;
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoResponses, ToSchema};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::types::Alarm;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema, ApiComponent)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct RequestBody {
|
pub struct RequestBody {
|
||||||
time: DateTime<Local>,
|
title: String,
|
||||||
|
time: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, JsonSchema, ApiComponent)]
|
#[derive(ToSchema, Serialize)]
|
||||||
pub struct SuccessResponse {
|
pub struct OkResponseBody {
|
||||||
pub time: DateTime<Local>,
|
id: i32,
|
||||||
pub enabled: bool
|
time: DateTime<Utc>,
|
||||||
|
title: String,
|
||||||
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, ApiErrorComponent)]
|
#[derive(IntoResponses)]
|
||||||
#[openapi_error(status(code = 500, description = "The alarm could not be created"))]
|
pub enum Responses {
|
||||||
pub enum ErrorResponse {
|
#[response(status = 200)]
|
||||||
InternalError(String)
|
Ok(#[to_schema] OkResponseBody),
|
||||||
|
#[response(status = 500)]
|
||||||
|
Error(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ErrorResponse {
|
impl IntoResponse for Responses {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn into_response(self) -> axum::response::Response {
|
||||||
match self {
|
match self {
|
||||||
ErrorResponse::InternalError(e) => write!(f, "Internal error: {}", e),
|
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||||
|
Responses::Error(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Alarm> for OkResponseBody {
|
||||||
|
fn from(value: Alarm) -> Self {
|
||||||
|
OkResponseBody {
|
||||||
|
time: value.time,
|
||||||
|
enabled: value.enabled,
|
||||||
|
title: value.title,
|
||||||
|
id: value.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for ErrorResponse {
|
#[utoipa::path(
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
post,
|
||||||
match self {
|
path = "",
|
||||||
ErrorResponse::InternalError(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
responses(
|
||||||
}
|
Responses
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
#[apistos::api_operation(
|
|
||||||
summary = "Add new alarm",
|
|
||||||
description = r###"Creates new Alarm"###,
|
|
||||||
error_code= 500,
|
|
||||||
)]
|
)]
|
||||||
pub async fn post(data: Data<AppState>, body: Json<RequestBody>) -> Result<CreatedJson<SuccessResponse>, ErrorResponse> {
|
#[debug_handler]
|
||||||
let result = data.scheduler.add_alarm(body.time);
|
pub async fn post_handler(State(AppState { scheduler, ..}): State<AppState>, Json(body): Json<RequestBody>) -> Responses {
|
||||||
|
let result = scheduler.add_alarm(body.time, body.title).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(alarm) => Ok(CreatedJson(SuccessResponse { time: alarm.time, enabled: alarm.enabled })),
|
Ok(alarm) => {
|
||||||
Err(e) => Err(ErrorResponse::InternalError(e)),
|
Responses::Ok(alarm.into())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
Responses::Error(e.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
use utoipa_axum::router::OpenApiRouter;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
mod alarm;
|
mod alarm;
|
||||||
|
|
||||||
pub fn v1() -> apistos::web::Scope {
|
pub fn router() -> OpenApiRouter<AppState> {
|
||||||
apistos::web::scope("/v1").service(alarm::resource())
|
OpenApiRouter::new().nest("/alarms", alarm::router())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
use std::{sync::{Arc, Mutex}, thread};
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
use gpio_cdev::{Chip, LineRequestFlags};
|
use gpio_cdev::{Chip, LineRequestFlags};
|
||||||
|
|
||||||
pub trait Ringer: Send + Sync {
|
pub trait Ringer: Send + Sync + Debug {
|
||||||
fn ring(&self) -> Result<(), String>;
|
fn ring(&self) -> Result<(), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,43 +16,40 @@ pub struct BeepRinger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BeepRinger {
|
impl BeepRinger {
|
||||||
|
fn beep(times: u32, chip: &mut Chip) -> Result<(), String> {
|
||||||
|
let beeper = chip.get_line(17);
|
||||||
|
|
||||||
fn beep(times: u32, chip: &mut Chip) -> Result<(), String> {
|
let beeper = match beeper {
|
||||||
let beeper = chip.get_line(17);
|
Ok(beeper) => beeper,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error opening line: {}", e);
|
||||||
|
return Err("Could not open Line to Beeper".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let beeper = match beeper {
|
let beeper = beeper.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
|
||||||
Ok(beeper) => beeper,
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error opening line: {}", e);
|
|
||||||
return Err("Could not open Line to Beeper".to_string());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let beeper = match beeper {
|
||||||
|
Ok(beeper) => beeper,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error requesting line: {}", e);
|
||||||
|
return Err("Could not request Line to Beeper".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let beeper = beeper
|
for _ in 0..times {
|
||||||
.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
|
beeper.set_value(1).map_err(|e| e.to_string())?;
|
||||||
|
thread::sleep(std::time::Duration::from_secs(1));
|
||||||
let beeper = match beeper {
|
beeper.set_value(0).map_err(|e| e.to_string())?;
|
||||||
Ok(beeper) => beeper,
|
thread::sleep(std::time::Duration::from_secs(1));
|
||||||
Err(e) => {
|
}
|
||||||
println!("Error requesting line: {}", e);
|
|
||||||
return Err("Could not request Line to Beeper".to_string());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for _ in 0..times {
|
|
||||||
beeper.set_value(1).map_err(|e| e.to_string())?;
|
|
||||||
thread::sleep(std::time::Duration::from_secs(1));
|
|
||||||
beeper.set_value(0).map_err(|e| e.to_string())?;
|
|
||||||
thread::sleep(std::time::Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(chip: Arc<Mutex<Chip>>) -> Self {
|
pub fn new(chip: Arc<Mutex<Chip>>) -> Self {
|
||||||
Self { chip }
|
Self { chip }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ringer for BeepRinger {
|
impl Ringer for BeepRinger {
|
||||||
@@ -63,8 +64,26 @@ impl Ringer for BeepRinger {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
BeepRinger::beep(5, &mut *chip)?;
|
BeepRinger::beep(5, &mut *chip)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for local testing without an actual beeper or similar. The only thing it does is print
|
||||||
|
/// that it's ringing.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SilentRinger;
|
||||||
|
|
||||||
|
impl SilentRinger {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ringer for SilentRinger {
|
||||||
|
fn ring(&self) -> Result<(), String> {
|
||||||
|
println!("Ringing");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/scheduler.rs
141
src/scheduler.rs
@@ -1,29 +1,40 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::{i32, usize};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use chrono::{DateTime, Local, Timelike};
|
use chrono::{DateTime, Local, Timelike, Utc};
|
||||||
use cron_tab::Cron;
|
use cron_tab::Cron;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr};
|
||||||
|
|
||||||
|
use crate::dao::alarm::{self, create_alarm, get_alarm, get_alarms};
|
||||||
|
use crate::model;
|
||||||
use crate::ringer::Ringer;
|
use crate::ringer::Ringer;
|
||||||
use crate::types::Alarm;
|
use crate::types::Alarm;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Scheduler<T: Ringer + 'static> {
|
pub struct Scheduler {
|
||||||
ringer: Arc<Mutex<T>>,
|
ringer: Arc<Mutex<dyn Ringer>>,
|
||||||
cron: Arc<Mutex<Cron<Local>>>,
|
cron: Arc<Mutex<Cron<Local>>>,
|
||||||
alarms: Arc<Mutex<Vec<Alarm>>>,
|
db: Arc<DatabaseConnection>,
|
||||||
|
cron_jobs: Arc<Mutex<HashMap<i32, usize>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Ringer> Scheduler<T> {
|
impl Scheduler {
|
||||||
pub fn new(ringer: Arc<Mutex<T>>, cron: Arc<Mutex<Cron<Local>>>) -> Self {
|
pub fn new(
|
||||||
|
ringer: Arc<Mutex<dyn Ringer>>,
|
||||||
|
cron: Arc<Mutex<Cron<Local>>>,
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ringer,
|
ringer,
|
||||||
cron,
|
cron,
|
||||||
alarms: Arc::new(Mutex::new(Vec::new())),
|
db,
|
||||||
|
cron_jobs: Arc::new(Mutex::new(HashMap::new()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn schedule(&self, cron_schedule: &str) -> Result<(), String> {
|
pub fn schedule(&self, cron_schedule: &str) -> Result<usize, String> {
|
||||||
let ringer = self.ringer.clone();
|
let ringer = self.ringer.clone();
|
||||||
let cron = self.cron.clone();
|
let cron = self.cron.clone();
|
||||||
|
|
||||||
@@ -37,22 +48,114 @@ impl<T: Ringer> Scheduler<T> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
job_result.expect("Faild to add job");
|
|
||||||
todo!()
|
Ok(job_result.expect("Faild to add job"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_alarm(&self, time: DateTime<Local>) -> Result<Alarm, String> {
|
pub async fn add_alarm(&self, time: DateTime<Utc>, title: String) -> Result<Alarm, String> {
|
||||||
let mut alarms = self.alarms.lock().map_err(|e| e.to_string())?;
|
let cron_schedule = self.construct_cron_schedule(time);
|
||||||
let cron_schedule = format!("{} {} {} * * *", "*", time.minute(), time.hour());
|
|
||||||
let alarm = Alarm::new(true, time);
|
let created_alarm = create_alarm(&*self.db, title, time)
|
||||||
alarms.push(alarm.clone());
|
.await
|
||||||
self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let job_id = self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
|
||||||
|
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(created_alarm.id, job_id);
|
||||||
println!("Added alarm {}", cron_schedule);
|
println!("Added alarm {}", cron_schedule);
|
||||||
|
|
||||||
Ok(alarm)
|
Ok(created_alarm.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&self) {
|
pub async fn get_alarms(&self, enabled: Option<bool>) -> Result<Vec<Alarm>, DbErr> {
|
||||||
|
get_alarms(&*self.db, enabled).await
|
||||||
|
.map(|alarms| alarms
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.into())
|
||||||
|
.collect()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_alarm(&self, id: i32, title: Option<String>, enabled: Option<bool>, time: Option<DateTime<Utc>>) -> Result<Option<Alarm>, DbErr> {
|
||||||
|
let alarm = get_alarm(&*self.db, id).await?;
|
||||||
|
|
||||||
|
let Some(alarm) = alarm else {
|
||||||
|
return Ok(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut active_alarm: model::alarm::ActiveModel = alarm.into();
|
||||||
|
|
||||||
|
if let Some(title) = title {
|
||||||
|
active_alarm.title = Set(title.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(enabled) = enabled {
|
||||||
|
active_alarm.enabled = Set(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(time) = time {
|
||||||
|
active_alarm.time = Set(time.naive_utc());
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_alarm = active_alarm.update(&*self.db).await?;
|
||||||
|
|
||||||
|
self.reregister_alarm(updated_alarm.clone().into());
|
||||||
|
|
||||||
|
return Ok(Some(updated_alarm.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&self) {
|
||||||
self.cron.lock().expect("Failed to lock cron").start();
|
self.cron.lock().expect("Failed to lock cron").start();
|
||||||
|
self.register_existing_alarms().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn register_existing_alarms(&self) {
|
||||||
|
let alarms_result = self.get_alarms(Some(true)).await;
|
||||||
|
|
||||||
|
match alarms_result {
|
||||||
|
Ok(alarms) => {
|
||||||
|
self.register_alarms(alarms);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get alarms: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_alarms(&self, alarms: Vec<Alarm>) {
|
||||||
|
alarms.into_iter().for_each(|a| {
|
||||||
|
self.register_alarm(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_alarm(&self, alarm: Alarm) {
|
||||||
|
let cron_schedule = self.construct_cron_schedule(alarm.time);
|
||||||
|
let scheduling_result = self.schedule(&cron_schedule);
|
||||||
|
|
||||||
|
match scheduling_result {
|
||||||
|
Ok(job_id) => {
|
||||||
|
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, job_id);
|
||||||
|
println!("Registered alarm {}", cron_schedule);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to register alarm {}: {}", cron_schedule, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_cron_schedule(&self, time: DateTime<Utc>) -> String {
|
||||||
|
let time = time.with_timezone(&Local);
|
||||||
|
format!("{} {} {} * * *", "*", time.minute(), time.hour())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reregister_alarm(&self, alarm: Alarm) {
|
||||||
|
let job_id = self.cron_jobs.lock().expect("Failed to lock cron_jobs")
|
||||||
|
.remove(&alarm.id);
|
||||||
|
|
||||||
|
if let Some(job_id) = job_id {
|
||||||
|
self.cron.lock().expect("Failed to lock cron handler").remove(job_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_job_id = self.schedule(&self.construct_cron_schedule(alarm.time)).expect("failed to register alarm");
|
||||||
|
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, new_job_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/types.rs
24
src/types.rs
@@ -1,16 +1,24 @@
|
|||||||
use apistos::ApiComponent;
|
use chrono::{DateTime, Utc};
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, ApiComponent)]
|
use crate::model;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Alarm {
|
pub struct Alarm {
|
||||||
|
pub id: i32,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub time: DateTime<Local>,
|
pub time: DateTime<Utc>,
|
||||||
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Alarm {
|
|
||||||
pub fn new(enabled: bool, time: DateTime<Local>) -> Self {
|
impl From<model::alarm::Model> for Alarm {
|
||||||
Self { enabled, time }
|
fn from(value: model::alarm::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
enabled: value.enabled,
|
||||||
|
time: DateTime::from_naive_utc_and_offset(value.time, Utc),
|
||||||
|
title: value.title,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user