Update
This commit is contained in:
parent
76b43a0386
commit
220a446fb6
21 changed files with 677 additions and 111 deletions
167
Cargo.lock
generated
167
Cargo.lock
generated
|
@ -548,6 +548,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
|
@ -666,7 +675,7 @@ version = "0.99.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
|
checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case 0.4.0",
|
||||||
"proc-macro2 1.0.89",
|
"proc-macro2 1.0.89",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
|
@ -884,6 +893,17 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flume"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"spin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -1204,10 +1224,10 @@ dependencies = [
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls 0.23.16",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.26.0",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
@ -1537,9 +1557,9 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"cron",
|
"cron",
|
||||||
|
"nutype",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"validated_newtype",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1553,16 +1573,42 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"cron",
|
"cron",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"envconfig",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"kdash_protocol",
|
"kdash_protocol",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rumqttc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kinded"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c"
|
||||||
|
dependencies = [
|
||||||
|
"kinded_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kinded_macros"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case 0.6.0",
|
||||||
|
"proc-macro2 1.0.89",
|
||||||
|
"quote 1.0.37",
|
||||||
|
"syn 2.0.87",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "language-tags"
|
name = "language-tags"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -1744,6 +1790,29 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nutype"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8789358e2d6cdffb0cb170c7802ee7548beb8067ed643f3122fa36c335f3c64"
|
||||||
|
dependencies = [
|
||||||
|
"nutype_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nutype_macros"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93a3e222ba1f06a03552910fe89a232a1661dcf8ad4c837531fb199828d0916b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"kinded",
|
||||||
|
"proc-macro2 1.0.89",
|
||||||
|
"quote 1.0.37",
|
||||||
|
"syn 2.0.87",
|
||||||
|
"urlencoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.5"
|
version = "0.36.5"
|
||||||
|
@ -1980,7 +2049,7 @@ dependencies = [
|
||||||
"quinn-proto",
|
"quinn-proto",
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls 0.23.16",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1997,7 +2066,7 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls 0.23.16",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
|
@ -2138,7 +2207,7 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls 0.23.16",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2148,7 +2217,7 @@ dependencies = [
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.26.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
|
@ -2175,6 +2244,24 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rumqttc"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"flume",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"rustls-webpki",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.25.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
@ -2209,6 +2296,20 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.16"
|
version = "0.23.16"
|
||||||
|
@ -2223,6 +2324,19 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -2453,6 +2567,9 @@ name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
|
@ -2672,13 +2789,24 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
|
||||||
|
dependencies = [
|
||||||
|
"rustls 0.22.4",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls 0.23.16",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
@ -2740,6 +2868,12 @@ version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -2764,6 +2898,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16_iter"
|
name = "utf16_iter"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -2791,15 +2931,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "validated_newtype"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "160f1ca3dc12c9e48f60b5b7a56092e372bda4ee2805f6c84ae337dd582fe12f"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
|
@ -25,6 +25,8 @@ reqwest = { version = "0.12.9", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"stream",
|
"stream",
|
||||||
|
"http2",
|
||||||
|
"charset",
|
||||||
] }
|
] }
|
||||||
tinybmp = "0.6.0"
|
tinybmp = "0.6.0"
|
||||||
tokio = { version = "1.41.0", features = ["full"] }
|
tokio = { version = "1.41.0", features = ["full"] }
|
||||||
|
|
12
kdash_client/extensions/kdash/config.xml
Normal file
12
kdash_client/extensions/kdash/config.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<extension>
|
||||||
|
<information>
|
||||||
|
<name>kdash</name>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<author>dergrimm</author>
|
||||||
|
<id>kdash</id>
|
||||||
|
</information>
|
||||||
|
<menus>
|
||||||
|
<menu type="json" dynamic="false">menu.json</menu>
|
||||||
|
</menus>
|
||||||
|
</extension>
|
|
@ -1,8 +1,9 @@
|
||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
DAEMON_PATH="/mnt/us/kdash"
|
DAEMON_PATH="/mnt/us/extensions/kdash"
|
||||||
|
|
||||||
DAEMON_ENV_FILE="${DAEMON_PATH}/kdash.env"
|
DAEMON_ENV_FILE="${DAEMON_PATH}/kdash.env"
|
||||||
|
DAEMON_ENABLED_FILE="${DAEMON_PATH}/ENABLED"
|
||||||
|
|
||||||
DAEMON="./kdash_client"
|
DAEMON="./kdash_client"
|
||||||
DAEMONOPTS=""
|
DAEMONOPTS=""
|
||||||
|
@ -13,7 +14,14 @@ PIDFILE="${DAEMON_PATH}/${DAEMON}.pid"
|
||||||
# SCRIPTNAME="/etc/init.d/${NAME}"
|
# SCRIPTNAME="/etc/init.d/${NAME}"
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
enable)
|
||||||
|
touch "$DAEMON_ENABLED_FILE"
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
rm -f "$DAEMON_ENABLED_FILE"
|
||||||
|
;;
|
||||||
start)
|
start)
|
||||||
|
if [ -e "$DAEMON_ENABLED_FILE" ]; then
|
||||||
printf "%-50s" "Starting $NAME..."
|
printf "%-50s" "Starting $NAME..."
|
||||||
cd "$DAEMON_PATH" || exit
|
cd "$DAEMON_PATH" || exit
|
||||||
. "$DAEMON_ENV_FILE"
|
. "$DAEMON_ENV_FILE"
|
||||||
|
@ -28,6 +36,9 @@ start)
|
||||||
echo "$PID" >"$PIDFILE"
|
echo "$PID" >"$PIDFILE"
|
||||||
printf "%s\n" "Ok"
|
printf "%s\n" "Ok"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo "Service not enabled. ENABLED file not found"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
printf "%-50s" "Checking $NAME..."
|
printf "%-50s" "Checking $NAME..."
|
13
kdash_client/extensions/kdash/menu.json
Normal file
13
kdash_client/extensions/kdash/menu.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "kdash",
|
||||||
|
"items": [
|
||||||
|
{ "name": "Enable", "priority": 0, "action": "./daemon.sh enable" },
|
||||||
|
{ "name": "Disable", "priority": 0, "action": "./daemon.sh disable" },
|
||||||
|
{ "name": "Start", "priority": 0, "action": "./daemon.sh start" },
|
||||||
|
{ "name": "Stop", "priority": 0, "action": "./daemon.sh stop" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
4
kdash_client/extensions/kdash/startup.sh
Normal file
4
kdash_client/extensions/kdash/startup.sh
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
sleep 120
|
||||||
|
/mnt/us/extensions/kdash/daemon.sh start
|
3
kdash_client/kite/onboot/kdash.sh
Normal file
3
kdash_client/kite/onboot/kdash.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
/mnt/us/extensions/kdash/startup.sh &
|
|
@ -2,6 +2,8 @@ use anyhow::{bail, Result};
|
||||||
use kdash_protocol::Orientation;
|
use kdash_protocol::Orientation;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::battery::BatteryStatus;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
pub jwt: String,
|
pub jwt: String,
|
||||||
|
@ -22,7 +24,7 @@ impl Api {
|
||||||
format!("Bearer {}", bearer)
|
format!("Bearer {}", bearer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_config(&self) -> Result<kdash_protocol::Config, reqwest::Error> {
|
pub async fn fetch_config(&self) -> Result<kdash_protocol::Config> {
|
||||||
let config = reqwest::Client::new()
|
let config = reqwest::Client::new()
|
||||||
.get(self.config_url.to_owned())
|
.get(self.config_url.to_owned())
|
||||||
.header(
|
.header(
|
||||||
|
@ -37,6 +39,30 @@ impl Api {
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_config_with_post_device_state(
|
||||||
|
&self,
|
||||||
|
battery: &BatteryStatus,
|
||||||
|
) -> Result<kdash_protocol::Config> {
|
||||||
|
let config = reqwest::Client::new()
|
||||||
|
.post(self.config_url.to_owned())
|
||||||
|
.header(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
Self::authorization_bearer(&self.jwt),
|
||||||
|
)
|
||||||
|
.json(&kdash_protocol::DeviceStatePost {
|
||||||
|
battery_charging: battery.is_charging,
|
||||||
|
battery_level: kdash_protocol::Percent::try_new(battery.percentage)?,
|
||||||
|
battery_current: battery.current,
|
||||||
|
battery_voltage: battery.voltage,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<kdash_protocol::Config>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fetch_image<'a>(
|
pub async fn fetch_image<'a>(
|
||||||
&self,
|
&self,
|
||||||
size: (u32, u32),
|
size: (u32, u32),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{net::IpAddr, path::PathBuf, rc::Rc, sync::Mutex};
|
use std::{net::IpAddr, rc::Rc, sync::Mutex};
|
||||||
|
|
||||||
use openlipc_dyn::Lipc;
|
use openlipc_dyn::Lipc;
|
||||||
|
|
||||||
|
@ -7,18 +7,11 @@ use crate::api;
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub api: api::Api,
|
pub api: api::Api,
|
||||||
pub app_config: AppConfig,
|
pub app_config: AppConfig,
|
||||||
pub config: Rc<Mutex<kdash_protocol::Config>>,
|
pub config: Rc<tokio::sync::Mutex<kdash_protocol::Config>>,
|
||||||
pub lipc: Rc<Mutex<Lipc>>,
|
pub lipc: Rc<Mutex<Lipc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub net: String,
|
pub net: String,
|
||||||
pub router_ip: IpAddr,
|
pub router_ip: IpAddr,
|
||||||
pub assets_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
pub fn asset(&self, path: &str) -> PathBuf {
|
|
||||||
self.assets_path.join(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub struct BatteryStatus {
|
||||||
pub is_charging: bool,
|
pub is_charging: bool,
|
||||||
pub percentage: u8,
|
pub percentage: u8,
|
||||||
pub current: i16,
|
pub current: i16,
|
||||||
|
pub voltage: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_battery_status() -> Result<BatteryStatus> {
|
pub fn get_battery_status() -> Result<BatteryStatus> {
|
||||||
|
@ -23,10 +24,14 @@ pub fn get_battery_status() -> Result<BatteryStatus> {
|
||||||
let charge_current_str = utils::exec_command("gasgauge-info", &["-l"])?;
|
let charge_current_str = utils::exec_command("gasgauge-info", &["-l"])?;
|
||||||
let charge_current = parse::parse_number_from_start_signed::<i16>(&charge_current_str)?;
|
let charge_current = parse::parse_number_from_start_signed::<i16>(&charge_current_str)?;
|
||||||
|
|
||||||
|
let voltage_str = utils::exec_command("gasgauge-info", &["-v"])?;
|
||||||
|
let voltage = parse::parse_number_from_start_signed::<i16>(&voltage_str)?;
|
||||||
|
|
||||||
let battery = BatteryStatus {
|
let battery = BatteryStatus {
|
||||||
is_charging,
|
is_charging,
|
||||||
percentage: charge,
|
percentage: charge,
|
||||||
current: charge_current,
|
current: charge_current,
|
||||||
|
voltage,
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("Got battery status: {:?}", battery);
|
log::info!("Got battery status: {:?}", battery);
|
||||||
|
@ -44,13 +49,13 @@ pub fn restart_powerd_config_condition(
|
||||||
battery_config: &kdash_protocol::BatteryConfig,
|
battery_config: &kdash_protocol::BatteryConfig,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if battery.is_charging
|
if battery.is_charging
|
||||||
&& battery.percentage <= *battery_config.restart_powerd_threshold
|
&& battery.percentage <= battery_config.restart_powerd_threshold.into_inner()
|
||||||
&& battery.current <= 0
|
&& battery.current <= 0
|
||||||
{
|
{
|
||||||
log::info!(
|
log::info!(
|
||||||
"Battery charge below threshold ({} <= {}): restarting powerd",
|
"Battery charge below threshold ({} <= {}): restarting powerd",
|
||||||
battery.percentage,
|
battery.percentage,
|
||||||
*battery_config.restart_powerd_threshold
|
battery_config.restart_powerd_threshold.into_inner()
|
||||||
);
|
);
|
||||||
restart_powerd()
|
restart_powerd()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use envconfig::Envconfig;
|
use envconfig::Envconfig;
|
||||||
use std::{net, path::PathBuf};
|
use std::net;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Envconfig, Debug)]
|
#[derive(Envconfig, Debug)]
|
||||||
|
@ -15,7 +15,4 @@ pub struct Config {
|
||||||
|
|
||||||
#[envconfig(from = "NET")]
|
#[envconfig(from = "NET")]
|
||||||
pub net: String,
|
pub net: String,
|
||||||
|
|
||||||
#[envconfig(from = "ASSETS")]
|
|
||||||
pub assets: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,9 @@ pub fn eips_clear() -> Result<()> {
|
||||||
utils::exec_command_discard("eips", &["-c"])
|
utils::exec_command_discard("eips", &["-c"])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn image_buf_to_raw<'a>(
|
pub fn image_buf_to_raw(
|
||||||
buf: &'a image::ImageBuffer<image::Luma<u8>, Vec<u8>>,
|
buf: &image::ImageBuffer<image::Luma<u8>, Vec<u8>>,
|
||||||
) -> ImageRawBE<'a, Gray8> {
|
) -> ImageRawBE<'_, Gray8> {
|
||||||
ImageRaw::new(buf.as_raw(), buf.dimensions().0)
|
ImageRaw::new(buf.as_raw(), buf.dimensions().0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,10 +186,7 @@ impl DrawTarget for FramebufferDisplay {
|
||||||
|
|
||||||
impl OriginDimensions for FramebufferDisplay {
|
impl OriginDimensions for FramebufferDisplay {
|
||||||
fn size(&self) -> Size {
|
fn size(&self) -> Size {
|
||||||
Size::new(
|
Size::new(self.fb_orientation.virtual_x, self.fb_orientation.virtual_y)
|
||||||
self.fb_orientation.virtual_x as u32,
|
|
||||||
self.fb_orientation.virtual_y as u32,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,18 +19,18 @@ pub async fn run_once(
|
||||||
state: &app::State,
|
state: &app::State,
|
||||||
display: &mut fb::FramebufferDisplay,
|
display: &mut fb::FramebufferDisplay,
|
||||||
) -> Result<Option<kdash_protocol::Config>> {
|
) -> Result<Option<kdash_protocol::Config>> {
|
||||||
let config = state.config.lock().unwrap();
|
let config = state.config.lock().await;
|
||||||
|
|
||||||
utils::set_cpu_powersaving()?;
|
utils::set_cpu_powersaving()?;
|
||||||
utils::prevent_screensaver(&state.lipc.lock().unwrap())?;
|
utils::prevent_screensaver(&state.lipc.lock().unwrap())?;
|
||||||
|
|
||||||
let battery = battery::get_battery_status()?;
|
let battery = battery::get_battery_status()?;
|
||||||
battery::restart_powerd_config_condition(&battery, &config.battery)?;
|
battery::restart_powerd_config_condition(&battery, &config.battery)?;
|
||||||
if battery.percentage <= *config.battery.low {
|
if battery.percentage <= config.battery.low.into_inner() {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Battery low: {}% <= {}%",
|
"Battery low: {}% <= {}%",
|
||||||
battery.percentage,
|
battery.percentage,
|
||||||
*config.battery.low
|
config.battery.low.into_inner()
|
||||||
);
|
);
|
||||||
|
|
||||||
Image::new(&*assets::ERROR_BATTERY_LOW_IMAGE, Point::zero())
|
Image::new(&*assets::ERROR_BATTERY_LOW_IMAGE, Point::zero())
|
||||||
|
@ -66,7 +66,11 @@ pub async fn run_once(
|
||||||
|
|
||||||
log::info!("Fetching config");
|
log::info!("Fetching config");
|
||||||
|
|
||||||
let fetch_succ = match state.api.fetch_config().await {
|
let fetch_succ = match state
|
||||||
|
.api
|
||||||
|
.fetch_config_with_post_device_state(&battery)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
new_config = Some(value);
|
new_config = Some(value);
|
||||||
|
|
||||||
|
@ -107,7 +111,7 @@ pub async fn run_once(
|
||||||
.draw(display)?;
|
.draw(display)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if battery.percentage <= *config.battery.alert {
|
if battery.percentage <= config.battery.alert.into_inner() {
|
||||||
let now_str = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
let now_str = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||||
let text = format!("Battery at {}%, please charge!", battery.percentage);
|
let text = format!("Battery at {}%, please charge!", battery.percentage);
|
||||||
let status_box = fb::widgets::status_box::StatusBox::new_with_default_style(
|
let status_box = fb::widgets::status_box::StatusBox::new_with_default_style(
|
||||||
|
|
|
@ -4,8 +4,6 @@ use envconfig::Envconfig;
|
||||||
use openlipc_dyn::Lipc;
|
use openlipc_dyn::Lipc;
|
||||||
use std::{rc::Rc, sync::Mutex, thread, time};
|
use std::{rc::Rc, sync::Mutex, thread, time};
|
||||||
|
|
||||||
use kdash_client;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
@ -13,7 +11,9 @@ async fn main() -> Result<()> {
|
||||||
let env_config = kdash_client::config::Config::init_from_env().unwrap();
|
let env_config = kdash_client::config::Config::init_from_env().unwrap();
|
||||||
|
|
||||||
let api = kdash_client::api::Api::new(env_config.kdash_jwt, env_config.kdash_url)?;
|
let api = kdash_client::api::Api::new(env_config.kdash_jwt, env_config.kdash_url)?;
|
||||||
let config = api.fetch_config().await?;
|
|
||||||
|
let battery = kdash_client::battery::get_battery_status()?;
|
||||||
|
let config = api.fetch_config_with_post_device_state(&battery).await?;
|
||||||
|
|
||||||
let lipc = Lipc::load(None)?;
|
let lipc = Lipc::load(None)?;
|
||||||
|
|
||||||
|
@ -22,15 +22,14 @@ async fn main() -> Result<()> {
|
||||||
app_config: kdash_client::app::AppConfig {
|
app_config: kdash_client::app::AppConfig {
|
||||||
net: env_config.net,
|
net: env_config.net,
|
||||||
router_ip: env_config.router_ip,
|
router_ip: env_config.router_ip,
|
||||||
assets_path: env_config.assets,
|
|
||||||
},
|
},
|
||||||
config: Rc::new(Mutex::new(config)),
|
config: Rc::new(tokio::sync::Mutex::new(config)),
|
||||||
lipc: Rc::new(Mutex::new(lipc)),
|
lipc: Rc::new(Mutex::new(lipc)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut display = kdash_client::fb::FramebufferDisplay::new(
|
let mut display = kdash_client::fb::FramebufferDisplay::new(
|
||||||
kdash_client::fb::DEFAULT_FB,
|
kdash_client::fb::DEFAULT_FB,
|
||||||
state.config.lock().unwrap().device.orientation,
|
state.config.lock().await.device.orientation,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
kdash_client::utils::kill_kindle()?;
|
kdash_client::utils::kill_kindle()?;
|
||||||
|
@ -44,7 +43,7 @@ async fn main() -> Result<()> {
|
||||||
match kdash_client::run_once(&state, &mut display).await {
|
match kdash_client::run_once(&state, &mut display).await {
|
||||||
Ok(Some(new_config)) => {
|
Ok(Some(new_config)) => {
|
||||||
log::info!("Updating config");
|
log::info!("Updating config");
|
||||||
let mut config = state.config.lock().unwrap();
|
let mut config = state.config.lock().await;
|
||||||
if display.fb_orientation.orientation != new_config.device.orientation {
|
if display.fb_orientation.orientation != new_config.device.orientation {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Updating orientation: {:?} -> {:?}",
|
"Updating orientation: {:?} -> {:?}",
|
||||||
|
@ -65,7 +64,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = state.config.lock().unwrap();
|
let config = state.config.lock().await;
|
||||||
|
|
||||||
thread::sleep(config.time.delay_before_suspend.to_std()?);
|
thread::sleep(config.time.delay_before_suspend.to_std()?);
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,6 @@ edition = "2021"
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
chrono-tz = { version = "0.10.0", features = ["serde"] }
|
chrono-tz = { version = "0.10.0", features = ["serde"] }
|
||||||
cron = { version = "0.13.0", features = ["serde"] }
|
cron = { version = "0.13.0", features = ["serde"] }
|
||||||
|
nutype = { version = "0.5.0", features = ["serde"] }
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
||||||
validated_newtype = "0.1.1"
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use cron::Schedule;
|
use cron::Schedule;
|
||||||
|
use nutype::nutype;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validated_newtype::validated_newtype;
|
|
||||||
|
|
||||||
validated_newtype! {
|
#[nutype(
|
||||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Clone, Copy)]
|
derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq),
|
||||||
u8 => pub Percent
|
validate(predicate = is_valid_percent)
|
||||||
if |n: &u8| *n <= 100;
|
)]
|
||||||
error "percent must in range 0-100"
|
pub struct Percent(u8);
|
||||||
|
|
||||||
|
fn is_valid_percent(n: &u8) -> bool {
|
||||||
|
*n <= 100
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||||
|
@ -66,3 +69,11 @@ pub enum Corner {
|
||||||
BottomLeft,
|
BottomLeft,
|
||||||
BottomRight,
|
BottomRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DeviceStatePost {
|
||||||
|
pub battery_charging: bool,
|
||||||
|
pub battery_level: Percent,
|
||||||
|
pub battery_current: i16,
|
||||||
|
pub battery_voltage: i16,
|
||||||
|
}
|
||||||
|
|
|
@ -11,11 +11,16 @@ chrono-tz = { version = "0.10.0", features = ["serde"] }
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
cron = { version = "0.13.0", features = ["serde"] }
|
cron = { version = "0.13.0", features = ["serde"] }
|
||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
|
envconfig = "0.11.0"
|
||||||
|
futures = "0.3.31"
|
||||||
|
futures-util = "0.3.31"
|
||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.0"
|
||||||
kdash_protocol = { path = "../kdash_protocol" }
|
kdash_protocol = { path = "../kdash_protocol" }
|
||||||
reqwest = { version = "0.12.9", features = ["stream"] }
|
reqwest = { version = "0.12.9", features = ["stream"] }
|
||||||
|
rumqttc = "0.24.0"
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
||||||
|
tokio = { version = "1.41.1", features = ["full"] }
|
||||||
url = { version = "2.5.3", features = ["serde"] }
|
url = { version = "2.5.3", features = ["serde"] }
|
||||||
uuid = { version = "1.11.0", features = ["serde"] }
|
uuid = { version = "1.11.0", features = ["serde"] }
|
||||||
|
|
22
kdash_server/src/config.rs
Normal file
22
kdash_server/src/config.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use envconfig::Envconfig;
|
||||||
|
|
||||||
|
#[derive(Envconfig, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
#[envconfig(from = "KDASH_MQTT_SERVER")]
|
||||||
|
pub kdash_mqtt_server: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "KDASH_MQTT_PORT")]
|
||||||
|
pub kdash_mqtt_port: u16,
|
||||||
|
|
||||||
|
#[envconfig(from = "KDASH_MQTT_USERNAME")]
|
||||||
|
pub kdash_mqtt_username: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "KDASH_MQTT_PASSWORD")]
|
||||||
|
pub kdash_mqtt_password: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "KDASH_MQTT_TOPIC")]
|
||||||
|
pub kdash_mqtt_topic: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "KDASH_MQTT_DISCOVERY_PREFIX")]
|
||||||
|
pub kdash_mqtt_discovery_prefix: Option<String>,
|
||||||
|
}
|
|
@ -1,31 +1,24 @@
|
||||||
use actix_web::{dev::PeerAddr, get, http, web, Error, HttpRequest, HttpResponse};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{AppState, Claims};
|
use actix_web::{dev::PeerAddr, get, post, web, Error, HttpResponse};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState, Claims, MqttClient, MqttDeviceDiscovery, MqttDeviceDiscoveryComponent,
|
||||||
|
MqttDeviceDiscoveryDevice, MqttDeviceDiscoveryOrigin, MqttState, VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/config")]
|
#[get("/config")]
|
||||||
pub async fn get_config(req: HttpRequest, data: web::Data<AppState>) -> HttpResponse {
|
pub async fn get_config(data: web::Data<AppState>, claims: Option<Claims>) -> HttpResponse {
|
||||||
let token = match req
|
let claims = match claims {
|
||||||
.headers()
|
Some(x) => x,
|
||||||
.get(http::header::AUTHORIZATION)
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.filter(|s| s.starts_with("Bearer "))
|
|
||||||
.map(|s| &s[7..])
|
|
||||||
{
|
|
||||||
Some(s) => s,
|
|
||||||
None => return HttpResponse::Unauthorized().finish(),
|
None => return HttpResponse::Unauthorized().finish(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let decoded =
|
if !data.device_ids.contains(&claims.sub) {
|
||||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
|
||||||
return HttpResponse::Unauthorized().finish();
|
return HttpResponse::Unauthorized().finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
let buf = match data.devices_api_json.get(&decoded.claims.sub) {
|
let buf = match data.devices_api_json.get(&claims.sub) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return HttpResponse::InternalServerError().finish(),
|
None => return HttpResponse::InternalServerError().finish(),
|
||||||
};
|
};
|
||||||
|
@ -35,35 +28,153 @@ pub async fn get_config(req: HttpRequest, data: web::Data<AppState>) -> HttpResp
|
||||||
.body(buf.clone())
|
.body(buf.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/config")]
|
||||||
|
pub async fn post_config(
|
||||||
|
claims: Option<Claims>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
mqtt_client: web::Data<MqttClient>,
|
||||||
|
device_state: web::Json<kdash_protocol::DeviceStatePost>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let claims = match claims {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return HttpResponse::Unauthorized().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !data.device_ids.contains(&claims.sub) {
|
||||||
|
return HttpResponse::Unauthorized().finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (config, config_buf) = match (
|
||||||
|
data.devices_api.get(&claims.sub),
|
||||||
|
data.devices_api_json.get(&claims.sub),
|
||||||
|
) {
|
||||||
|
(Some(config), Some(config_buf)) => (config, config_buf),
|
||||||
|
_ => return HttpResponse::InternalServerError().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_id = claims.sub.to_string();
|
||||||
|
|
||||||
|
let state_topic = format!("{}/device/{}/state", mqtt_client.topic, device_id);
|
||||||
|
|
||||||
|
let state_payload = match serde_json::to_vec(&MqttState {
|
||||||
|
battery_charging: device_state.battery_charging,
|
||||||
|
battery_level: device_state.battery_level,
|
||||||
|
battery_current: device_state.battery_current,
|
||||||
|
battery_voltage: device_state.battery_voltage,
|
||||||
|
}) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if mqtt_client
|
||||||
|
.client
|
||||||
|
.publish(
|
||||||
|
&state_topic,
|
||||||
|
rumqttc::v5::mqttbytes::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
|
state_payload,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(discovery_topic) = &mqtt_client.discovery_topic {
|
||||||
|
let unique_id = format!("kdash_{}", device_id);
|
||||||
|
let unique_id_battery_level = format!("{}_battery_level", unique_id);
|
||||||
|
let unique_id_battery_current = format!("{}_battery_current", unique_id);
|
||||||
|
let unique_id_battery_voltage = format!("{}_battery_voltage", unique_id);
|
||||||
|
|
||||||
|
let mut components = HashMap::new();
|
||||||
|
components.insert(
|
||||||
|
"battery_level".to_string(),
|
||||||
|
MqttDeviceDiscoveryComponent {
|
||||||
|
platform: "sensor",
|
||||||
|
device_class: "battery",
|
||||||
|
unit_of_measurement: Some("%"),
|
||||||
|
value_template: "{{ value_json.battery_level }}",
|
||||||
|
unique_id: &unique_id_battery_level,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
components.insert(
|
||||||
|
"battery_current".to_string(),
|
||||||
|
MqttDeviceDiscoveryComponent {
|
||||||
|
platform: "sensor",
|
||||||
|
device_class: "current",
|
||||||
|
unit_of_measurement: Some("mA"),
|
||||||
|
value_template: "{{ value_json.battery_current }}",
|
||||||
|
unique_id: &unique_id_battery_current,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
components.insert(
|
||||||
|
"battery_voltage".to_string(),
|
||||||
|
MqttDeviceDiscoveryComponent {
|
||||||
|
platform: "sensor",
|
||||||
|
device_class: "voltage",
|
||||||
|
unit_of_measurement: Some("mV"),
|
||||||
|
value_template: "{{ value_json.battery_voltage }}",
|
||||||
|
unique_id: &unique_id_battery_voltage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let discovery = MqttDeviceDiscovery {
|
||||||
|
device: MqttDeviceDiscoveryDevice {
|
||||||
|
identifiers: &unique_id,
|
||||||
|
name: config.name.to_owned(),
|
||||||
|
},
|
||||||
|
origin: MqttDeviceDiscoveryOrigin {
|
||||||
|
name: "kdash",
|
||||||
|
software_version: VERSION,
|
||||||
|
url: "https://git.dergrimm.net/dergrimm/kdash",
|
||||||
|
},
|
||||||
|
components,
|
||||||
|
state_topic: &state_topic,
|
||||||
|
qos: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let discovery_payload = match serde_json::to_vec(&discovery) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if mqtt_client
|
||||||
|
.client
|
||||||
|
.publish(
|
||||||
|
format!("{}/device/kdash/{}/config", discovery_topic, device_id),
|
||||||
|
rumqttc::v5::mqttbytes::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
|
discovery_payload,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(config_buf.clone())
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/image")]
|
#[get("/image")]
|
||||||
pub async fn get_image(
|
pub async fn get_image(
|
||||||
req: HttpRequest,
|
claims: Option<Claims>,
|
||||||
peer_addr: Option<PeerAddr>,
|
peer_addr: Option<PeerAddr>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
client: web::Data<reqwest::Client>,
|
client: web::Data<reqwest::Client>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let token = match req
|
let claims = match claims {
|
||||||
.headers()
|
Some(x) => x,
|
||||||
.get(http::header::AUTHORIZATION)
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.filter(|s| s.starts_with("Bearer "))
|
|
||||||
.map(|s| &s[7..])
|
|
||||||
{
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Ok(HttpResponse::Unauthorized().finish()),
|
None => return Ok(HttpResponse::Unauthorized().finish()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let decoded =
|
if !data.device_ids.contains(&claims.sub) {
|
||||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(_) => return Ok(HttpResponse::BadRequest().finish()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match data.devices.get(&decoded.claims.sub) {
|
let config = match data.devices.get(&claims.sub) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Ok(HttpResponse::InternalServerError().finish()),
|
None => return Ok(HttpResponse::InternalServerError().finish()),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
|
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
||||||
|
use actix_web::{http, FromRequest, HttpMessage};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use futures::future::{err, ok, LocalBoxFuture};
|
||||||
use jsonwebtoken::{DecodingKey, Validation};
|
use jsonwebtoken::{DecodingKey, Validation};
|
||||||
|
use kdash_protocol::Percent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::future::{ready, Ready};
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub devices: HashMap<Uuid, device::Config>,
|
pub devices: HashMap<Uuid, device::Config>,
|
||||||
|
@ -35,3 +45,177 @@ pub struct Claims {
|
||||||
pub exp: i64,
|
pub exp: i64,
|
||||||
pub sub: Uuid,
|
pub sub: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRequest for Claims {
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = futures::future::Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(
|
||||||
|
req: &actix_web::HttpRequest,
|
||||||
|
_payload: &mut actix_web::dev::Payload,
|
||||||
|
) -> Self::Future {
|
||||||
|
match req.extensions().get::<Claims>() {
|
||||||
|
Some(claims) => ok(claims.clone()),
|
||||||
|
None => err(actix_web::error::ErrorUnauthorized("Unauthorized")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Authority {
|
||||||
|
pub decoding_key: DecodingKey,
|
||||||
|
pub validation: Validation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JwtAuth<Claims> {
|
||||||
|
pub auth: Arc<Authority>,
|
||||||
|
claims_marker: PhantomData<Claims>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Claims> JwtAuth<Claims> {
|
||||||
|
pub fn new(auth: Arc<Authority>) -> Self {
|
||||||
|
Self {
|
||||||
|
auth,
|
||||||
|
claims_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B, Claims> Transform<S, ServiceRequest> for JwtAuth<Claims>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = JwtAuthMiddleware<S, Claims>;
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(JwtAuthMiddleware {
|
||||||
|
service,
|
||||||
|
auth: Arc::clone(&self.auth),
|
||||||
|
claims_marker: PhantomData,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JwtAuthMiddleware<S, Claims> {
|
||||||
|
service: S,
|
||||||
|
auth: Arc<Authority>,
|
||||||
|
claims_marker: PhantomData<Claims>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B, C> Service<ServiceRequest> for JwtAuthMiddleware<S, C>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let decoded = req
|
||||||
|
.headers()
|
||||||
|
.get(http::header::AUTHORIZATION)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.filter(|s| s.starts_with("Bearer "))
|
||||||
|
.map(|s| &s[7..])
|
||||||
|
.and_then(|token| {
|
||||||
|
jsonwebtoken::decode::<Claims>(
|
||||||
|
token,
|
||||||
|
&self.auth.decoding_key,
|
||||||
|
&self.auth.validation,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(data) = decoded {
|
||||||
|
req.extensions_mut().insert(data.claims);
|
||||||
|
dbg!(req.extensions());
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::pin(self.service.call(req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MqttClient {
|
||||||
|
pub client: rumqttc::v5::AsyncClient,
|
||||||
|
pub topic: String,
|
||||||
|
pub discovery_topic: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MqttState {
|
||||||
|
pub battery_charging: bool,
|
||||||
|
pub battery_level: Percent,
|
||||||
|
pub battery_current: i16,
|
||||||
|
pub battery_voltage: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MqttDeviceDiscovery<'a> {
|
||||||
|
#[serde(rename = "dev")]
|
||||||
|
pub device: MqttDeviceDiscoveryDevice<'a>,
|
||||||
|
|
||||||
|
#[serde(rename = "o")]
|
||||||
|
pub origin: MqttDeviceDiscoveryOrigin<'a>,
|
||||||
|
|
||||||
|
#[serde(rename = "cmps")]
|
||||||
|
pub components: HashMap<String, MqttDeviceDiscoveryComponent<'a>>,
|
||||||
|
|
||||||
|
pub state_topic: &'a str,
|
||||||
|
|
||||||
|
pub qos: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MqttDeviceDiscoveryDevice<'a> {
|
||||||
|
#[serde(rename = "ids")]
|
||||||
|
pub identifiers: &'a str,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
// #[serde(rename = "mf")]
|
||||||
|
// pub manufacturer: &'a str,
|
||||||
|
|
||||||
|
// #[serde(rename = "mdl")]
|
||||||
|
// pub default_manufacturer: &'a str,
|
||||||
|
|
||||||
|
// #[serde(rename = "sw")]
|
||||||
|
// pub sw_version: &'a str,
|
||||||
|
|
||||||
|
// #[serde(rename = "sn")]
|
||||||
|
// pub serial_number: &'a str,
|
||||||
|
|
||||||
|
// #[serde(rename = "hw")]
|
||||||
|
// pub hw_version: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MqttDeviceDiscoveryOrigin<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
|
||||||
|
#[serde(rename = "sw")]
|
||||||
|
pub software_version: &'a str,
|
||||||
|
|
||||||
|
pub url: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MqttDeviceDiscoveryComponent<'a> {
|
||||||
|
#[serde(rename = "p")]
|
||||||
|
pub platform: &'a str,
|
||||||
|
pub device_class: &'a str,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub unit_of_measurement: Option<&'a str>,
|
||||||
|
|
||||||
|
pub value_template: &'a str,
|
||||||
|
pub unique_id: &'a str,
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,14 @@ use actix_web::{middleware::Logger, web, App, HttpServer};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use envconfig::Envconfig;
|
||||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -40,6 +43,8 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let env_config = kdash_server::config::Config::init_from_env()?;
|
||||||
|
|
||||||
let config = kdash_server::read_config(&cli.config)?;
|
let config = kdash_server::read_config(&cli.config)?;
|
||||||
|
|
||||||
let sk = fs::read(cli.private_key)?;
|
let sk = fs::read(cli.private_key)?;
|
||||||
|
@ -61,6 +66,25 @@ async fn main() -> Result<()> {
|
||||||
println!("{}", token);
|
println!("{}", token);
|
||||||
}
|
}
|
||||||
Commands::Start => {
|
Commands::Start => {
|
||||||
|
let mut mqtt_options = rumqttc::v5::MqttOptions::new(
|
||||||
|
"kdash-server",
|
||||||
|
env_config.kdash_mqtt_server,
|
||||||
|
env_config.kdash_mqtt_port,
|
||||||
|
);
|
||||||
|
mqtt_options.set_credentials(
|
||||||
|
env_config.kdash_mqtt_username,
|
||||||
|
env_config.kdash_mqtt_password,
|
||||||
|
);
|
||||||
|
mqtt_options.set_keep_alive(Duration::from_secs(10));
|
||||||
|
|
||||||
|
let (mqtt_client, mut eventloop) = rumqttc::v5::AsyncClient::new(mqtt_options, 20);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
eventloop.poll().await.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let devices = config.devices.to_owned();
|
let devices = config.devices.to_owned();
|
||||||
let device_ids: HashSet<Uuid> = config.devices.keys().cloned().collect();
|
let device_ids: HashSet<Uuid> = config.devices.keys().cloned().collect();
|
||||||
|
@ -88,8 +112,20 @@ async fn main() -> Result<()> {
|
||||||
jwt_validation: Validation::new(ALGORITHM),
|
jwt_validation: Validation::new(ALGORITHM),
|
||||||
}))
|
}))
|
||||||
.app_data(web::Data::new(reqwest_client))
|
.app_data(web::Data::new(reqwest_client))
|
||||||
|
.app_data(web::Data::new(kdash_server::MqttClient {
|
||||||
|
client: mqtt_client.to_owned(),
|
||||||
|
topic: env_config.kdash_mqtt_topic.to_owned(),
|
||||||
|
discovery_topic: env_config.kdash_mqtt_discovery_prefix.to_owned(),
|
||||||
|
}))
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
|
.wrap(kdash_server::JwtAuth::<kdash_server::Claims>::new(
|
||||||
|
Arc::new(kdash_server::Authority {
|
||||||
|
decoding_key: decoding_key.to_owned(),
|
||||||
|
validation: Validation::new(ALGORITHM),
|
||||||
|
}),
|
||||||
|
))
|
||||||
.service(kdash_server::handlers::get_config)
|
.service(kdash_server::handlers::get_config)
|
||||||
|
.service(kdash_server::handlers::post_config)
|
||||||
.service(kdash_server::handlers::get_image)
|
.service(kdash_server::handlers::get_image)
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", 8080))?
|
.bind(("0.0.0.0", 8080))?
|
||||||
|
|
Loading…
Reference in a new issue