Compare commits
15
Commits
master
..
9bfafbff87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bfafbff87
|
||
|
|
956f818a3b
|
||
|
|
a0c5189ada
|
||
|
|
3676aafa23
|
||
|
|
95ec3f28ff
|
||
|
|
303541929c
|
||
|
|
a26a93550b
|
||
|
|
88a7acd55d
|
||
|
|
3b7579878b
|
||
|
|
46c32c3605
|
||
|
|
d1e7988117
|
||
|
|
93b0a526b1
|
||
|
|
7bb5e65c1c
|
||
|
|
a0ed373971
|
||
|
|
5e13dff2b5
|
@@ -1,5 +1,2 @@
|
|||||||
[env]
|
[env]
|
||||||
RUST_LOG = "automation=debug"
|
RUST_LOG = "automation=debug"
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-musl]
|
|
||||||
rustflags = ["-C", "link-arg=-lc"]
|
|
||||||
|
|||||||
@@ -2,5 +2,3 @@
|
|||||||
.env
|
.env
|
||||||
# Use the rust environment provided by the container
|
# Use the rust environment provided by the container
|
||||||
rust-toolchain.toml
|
rust-toolchain.toml
|
||||||
Dockerfile
|
|
||||||
docker-bake.hcl
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Audit
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- "**/Cargo.toml"
|
|
||||||
- "**/Cargo.lock"
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: cargo audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Install rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@nightly
|
|
||||||
- name: Setup rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Audit check
|
|
||||||
uses: https://git.huizinga.dev/infra/rust-audit-check@v3.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
@@ -9,10 +9,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
uses: infra/workflows/.gitea/workflows/docker.yaml@2bb37b698effa4fba36040d668cfa95d4abc568e
|
uses: dreaded_x/workflows/.gitea/workflows/rust-kubernetes.yaml@22ee0c1788a8d2157db87d6a6f8dbe520fe48592
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
push_manifests: false
|
upload_manifests: false
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy container
|
name: Deploy container
|
||||||
@@ -26,10 +26,6 @@ jobs:
|
|||||||
docker stop automation_rs || true
|
docker stop automation_rs || true
|
||||||
docker rm automation_rs || true
|
docker rm automation_rs || true
|
||||||
|
|
||||||
- name: Login to registry
|
|
||||||
run: |
|
|
||||||
docker login git.huizinga.dev -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} \
|
|
||||||
|
|
||||||
- name: Create container
|
- name: Create container
|
||||||
run: |
|
run: |
|
||||||
docker create \
|
docker create \
|
||||||
@@ -41,9 +37,7 @@ jobs:
|
|||||||
-e AUTOMATION__SECRETS__MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \
|
-e AUTOMATION__SECRETS__MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \
|
||||||
-e AUTOMATION__SECRETS__HUE_TOKEN=${{ secrets.HUE_TOKEN }} \
|
-e AUTOMATION__SECRETS__HUE_TOKEN=${{ secrets.HUE_TOKEN }} \
|
||||||
-e AUTOMATION__SECRETS__NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \
|
-e AUTOMATION__SECRETS__NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \
|
||||||
-e AUTOMATION__SECRETS__PRINTER_DEVICE_ID=${{ secrets.PRINTER_DEVICE_ID }} \
|
git.huizinga.dev/dreaded_x/automation_rs@${{ needs.build.outputs.digest }}
|
||||||
-e AUTOMATION__SECRETS__PRINTER_ACCESS_CODE=${{ secrets.PRINTER_ACCESS_CODE }} \
|
|
||||||
$(echo ${{ toJSON(needs.build.outputs.images) }} | jq .automation -r)
|
|
||||||
|
|
||||||
docker network connect web automation_rs
|
docker network connect web automation_rs
|
||||||
|
|
||||||
|
|||||||
Generated
+688
-1041
File diff suppressed because it is too large
Load Diff
+23
-23
@@ -16,28 +16,28 @@ members = [
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
|
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.99"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
automation_cast = { path = "./automation_cast" }
|
automation_cast = { path = "./automation_cast" }
|
||||||
automation_devices = { path = "./automation_devices" }
|
automation_devices = { path = "./automation_devices" }
|
||||||
automation_lib = { path = "./automation_lib" }
|
automation_lib = { path = "./automation_lib" }
|
||||||
automation_macro = { path = "./automation_macro" }
|
automation_macro = { path = "./automation_macro" }
|
||||||
axum = "0.8.9"
|
axum = "0.8.4"
|
||||||
bytes = "1.11.1"
|
bytes = "1.10.1"
|
||||||
dyn-clone = "1.0.20"
|
dyn-clone = "1.0.20"
|
||||||
eui48 = { version = "1.1.0", features = [
|
eui48 = { version = "1.1.0", features = [
|
||||||
"disp_hexstring",
|
"disp_hexstring",
|
||||||
"serde",
|
"serde",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
futures = "0.3.32"
|
futures = "0.3.31"
|
||||||
google_home = { path = "./google_home/google_home" }
|
google_home = { path = "./google_home/google_home" }
|
||||||
google_home_macro = { path = "./google_home/google_home_macro" }
|
google_home_macro = { path = "./google_home/google_home_macro" }
|
||||||
hostname = "0.4.2"
|
hostname = "0.4.1"
|
||||||
inventory = "0.3.24"
|
inventory = "0.3.21"
|
||||||
itertools = "0.15.0"
|
itertools = "0.14.0"
|
||||||
json_value_merge = "2.0.1"
|
json_value_merge = "2.0.1"
|
||||||
lua_typed = { git = "https://git.huizinga.dev/Dreaded_X/lua_typed" }
|
lua_typed = { git = "https://git.huizinga.dev/Dreaded_X/lua_typed" }
|
||||||
mlua = { version = "0.12.0", features = [
|
mlua = { version = "0.11.3", features = [
|
||||||
"lua54",
|
"lua54",
|
||||||
"vendored",
|
"vendored",
|
||||||
"macros",
|
"macros",
|
||||||
@@ -45,23 +45,23 @@ mlua = { version = "0.12.0", features = [
|
|||||||
"async",
|
"async",
|
||||||
"send",
|
"send",
|
||||||
] }
|
] }
|
||||||
proc-macro2 = "1.0.106"
|
proc-macro2 = "1.0.101"
|
||||||
quote = "1.0.45"
|
quote = "1.0.40"
|
||||||
reqwest = { version = "0.13.3", features = [
|
reqwest = { version = "0.12.23", features = [
|
||||||
"json",
|
"json",
|
||||||
"rustls",
|
"rustls-tls",
|
||||||
], default-features = false } # Use rustls, since the other packages also use rustls
|
], default-features = false } # Use rustls, since the other packages also use rustls
|
||||||
rumqttc-next = "0.34.0"
|
rumqttc = "0.24.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.143"
|
||||||
serde_repr = "0.1.20"
|
serde_repr = "0.1.20"
|
||||||
syn = { version = "3.0.3" }
|
syn = { version = "2.0.106" }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.16"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
tokio-cron-scheduler = "0.15.1"
|
tokio-cron-scheduler = "0.15.0"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.23"
|
tracing-subscriber = "0.3.20"
|
||||||
wakey = "0.4.1"
|
wakey = "0.3.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
@@ -70,7 +70,7 @@ automation_devices = { workspace = true }
|
|||||||
automation_lib = { workspace = true }
|
automation_lib = { workspace = true }
|
||||||
automation_macro = { path = "./automation_macro" }
|
automation_macro = { path = "./automation_macro" }
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
config = { version = "0.15.22", default-features = false, features = [
|
config = { version = "0.15.15", default-features = false, features = [
|
||||||
"async",
|
"async",
|
||||||
"toml",
|
"toml",
|
||||||
] }
|
] }
|
||||||
@@ -80,6 +80,7 @@ lua_typed = { workspace = true }
|
|||||||
inventory = { workspace = true }
|
inventory = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
|
rumqttc = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
@@ -87,7 +88,6 @@ tokio = { workspace = true }
|
|||||||
tokio-cron-scheduler = { workspace = true }
|
tokio-cron-scheduler = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
rumqttc-next = { workspace = true }
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
||||||
|
|||||||
+3
-7
@@ -1,15 +1,14 @@
|
|||||||
FROM rust:1.98-alpine3.24 AS base
|
FROM rust:1.89 AS base
|
||||||
|
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||||
RUN cargo install cargo-chef --locked --version 0.1.71 && \
|
RUN cargo install cargo-chef --locked --version 0.1.71 && \
|
||||||
cargo install cargo-auditable --locked --version 0.6.6
|
cargo install cargo-auditable --locked --version 0.6.6
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN rustup toolchain install
|
|
||||||
|
|
||||||
FROM base AS planner
|
FROM base AS planner
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo chef prepare --recipe-path recipe.json
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache g++=15.2.0-r5 cmake=4.2.3-r0 make=4.4.1-r4 openssl-dev=3.5.8-r0 openssl-libs-static=3.5.8-r0
|
|
||||||
# HACK: Now we can use unstable feature while on stable rust!
|
# HACK: Now we can use unstable feature while on stable rust!
|
||||||
ENV RUSTC_BOOTSTRAP=1
|
ENV RUSTC_BOOTSTRAP=1
|
||||||
COPY --from=planner /app/recipe.json recipe.json
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
@@ -20,10 +19,7 @@ ARG RELEASE_VERSION
|
|||||||
ENV RELEASE_VERSION=${RELEASE_VERSION}
|
ENV RELEASE_VERSION=${RELEASE_VERSION}
|
||||||
RUN cargo auditable build --release
|
RUN cargo auditable build --release
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime
|
||||||
FROM gcr.io/distroless/static-debian13:nonroot AS runtime
|
|
||||||
COPY --from=builder /app/target/release/automation /app/automation
|
COPY --from=builder /app/target/release/automation /app/automation
|
||||||
ENV AUTOMATION__ENTRYPOINT=/app/config/config.lua
|
|
||||||
ENV LUA_PATH="/app/?.lua;;"
|
|
||||||
COPY ./config /app/config
|
COPY ./config /app/config
|
||||||
CMD [ "/app/automation" ]
|
CMD [ "/app/automation" ]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ Custom home automation solution with Google Home integration and lua scripting.
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
This repository uses [prek](https://prek.j178.dev/) to make sure everything is ready to go when committing.
|
This repository uses [pre-commit](https://pre-commit.com) to make sure everything is ready to go when committing.
|
||||||
Install the pre-commit hooks by running the following command:
|
Install the pre-commit hooks by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
prek install
|
pre-commit install
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ inventory = { workspace = true }
|
|||||||
lua_typed = { workspace = true }
|
lua_typed = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rumqttc-next = { workspace = true }
|
rumqttc = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_repr = { workspace = true }
|
serde_repr = { workspace = true }
|
||||||
@@ -25,4 +25,3 @@ thiserror = { workspace = true }
|
|||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
wakey = { workspace = true }
|
wakey = { workspace = true }
|
||||||
bambulab = { version = "0.4.30", default-features = false }
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
use std::convert::Infallible;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use automation_lib::action_callback::ActionCallback;
|
|
||||||
use automation_lib::device::Device;
|
|
||||||
use automation_macro::{Device, LuaDeviceConfig};
|
|
||||||
use bambulab::client::Client;
|
|
||||||
use bambulab::{Command, Message};
|
|
||||||
use google_home::errors::{self};
|
|
||||||
use google_home::traits::OnOff;
|
|
||||||
use lua_typed::Typed;
|
|
||||||
use tracing::{debug, trace};
|
|
||||||
|
|
||||||
use crate::{DebugWrap, LuaDeviceCreate};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, Typed, Default)]
|
|
||||||
#[typed(as = "BambuCallbacks")]
|
|
||||||
pub struct Callbacks {
|
|
||||||
#[device_config(from_lua, default)]
|
|
||||||
#[typed(default)]
|
|
||||||
pub state: ActionCallback<Bambu>,
|
|
||||||
#[device_config(from_lua, default)]
|
|
||||||
#[typed(default)]
|
|
||||||
pub connected: ActionCallback<Bambu>,
|
|
||||||
}
|
|
||||||
crate::register_type!(Callbacks);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
|
|
||||||
#[typed(as = "BambuConfig")]
|
|
||||||
pub struct Config {
|
|
||||||
pub host: String,
|
|
||||||
pub device_id: String,
|
|
||||||
pub access_code: String,
|
|
||||||
#[device_config(from_lua, default)]
|
|
||||||
pub callbacks: Callbacks,
|
|
||||||
}
|
|
||||||
crate::register_type!(Config);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Device)]
|
|
||||||
#[device(traits(OnOff))]
|
|
||||||
pub struct Bambu {
|
|
||||||
config: Config,
|
|
||||||
|
|
||||||
client: DebugWrap<Client>,
|
|
||||||
|
|
||||||
state: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
crate::register_device!(Bambu);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl LuaDeviceCreate for Bambu {
|
|
||||||
type Config = Config;
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
async fn create(config: Self::Config) -> Result<Self, Infallible> {
|
|
||||||
trace!(id = config.device_id, "Setting up bambu");
|
|
||||||
|
|
||||||
let (tx, mut rx) = tokio::sync::broadcast::channel(25);
|
|
||||||
let client = Client::new(&config.host, &config.access_code, &config.device_id, tx);
|
|
||||||
|
|
||||||
let state = Arc::new(AtomicBool::new(false));
|
|
||||||
let bambu = Self {
|
|
||||||
config,
|
|
||||||
client: DebugWrap(client.clone()),
|
|
||||||
state: state.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::spawn({
|
|
||||||
let mut bambu = bambu.clone();
|
|
||||||
async move {
|
|
||||||
// The printer might be offline so periodically try to reconnecct
|
|
||||||
loop {
|
|
||||||
bambu.client.run().await.ok();
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn({
|
|
||||||
let bambu = bambu.clone();
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
let message = rx.recv().await.unwrap();
|
|
||||||
|
|
||||||
match message {
|
|
||||||
Message::Print(data) => 'print: {
|
|
||||||
// Extract the state of the chamber light
|
|
||||||
let Some(light_report) = data.print.lights_report else {
|
|
||||||
break 'print;
|
|
||||||
};
|
|
||||||
|
|
||||||
let on = light_report
|
|
||||||
.iter()
|
|
||||||
.find(|report| report.node == "chamber_light")
|
|
||||||
.map(|report| report.mode == "on")
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
state.store(on, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
|
|
||||||
bambu.config.callbacks.state.call(bambu.clone()).await;
|
|
||||||
}
|
|
||||||
Message::Connected => {
|
|
||||||
debug!(id = bambu.config.device_id, "Connected");
|
|
||||||
client.publish(Command::PushAll).await.unwrap();
|
|
||||||
|
|
||||||
bambu.config.callbacks.connected.call(bambu.clone()).await;
|
|
||||||
}
|
|
||||||
// Ignore everything else
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(bambu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device for Bambu {
|
|
||||||
fn get_id(&self) -> String {
|
|
||||||
self.config.device_id.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl OnOff for Bambu {
|
|
||||||
async fn on(&self) -> Result<bool, errors::ErrorCode> {
|
|
||||||
Ok(self.state.load(std::sync::atomic::Ordering::Relaxed))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_on(&self, on: bool) -> Result<(), errors::ErrorCode> {
|
|
||||||
// NOTE: This will error in case the printer is offline, but we don't really care in that
|
|
||||||
// case so we just ignore the error
|
|
||||||
self.client.publish(Command::SetChamberLight(on)).await.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -159,10 +159,7 @@ impl OpenClose for ContactSensor {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for ContactSensor {
|
impl OnMqtt for ContactSensor {
|
||||||
async fn on_mqtt(&self, message: rumqttc::Publish) {
|
async fn on_mqtt(&self, message: rumqttc::Publish) {
|
||||||
if !rumqttc::matches(
|
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::net::SocketAddr;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_lib::lua::traits::PartialUserData;
|
|
||||||
use automation_macro::{Device, LuaDeviceConfig};
|
use automation_macro::{Device, LuaDeviceConfig};
|
||||||
use google_home::errors::ErrorCode;
|
use google_home::errors::ErrorCode;
|
||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
@@ -26,7 +25,6 @@ crate::register_type!(Config);
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Device)]
|
#[derive(Debug, Clone, Device)]
|
||||||
#[device(traits(OnOff))]
|
#[device(traits(OnOff))]
|
||||||
#[device(extra_user_data = AllOn)]
|
|
||||||
pub struct HueGroup {
|
pub struct HueGroup {
|
||||||
config: Config,
|
config: Config,
|
||||||
}
|
}
|
||||||
@@ -124,47 +122,6 @@ impl OnOff for HueGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AllOn;
|
|
||||||
impl PartialUserData<HueGroup> for AllOn {
|
|
||||||
fn add_methods<M: mlua::UserDataMethods<HueGroup>>(methods: &mut M) {
|
|
||||||
methods.add_async_method("all_on", async |_lua, this, ()| {
|
|
||||||
let res = reqwest::Client::new()
|
|
||||||
.get(this.url_get_state())
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(res) => {
|
|
||||||
let status = res.status();
|
|
||||||
if !status.is_success() {
|
|
||||||
warn!(id = this.get_id(), "Status code is not success: {status}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let on = match res.json::<message::Info>().await {
|
|
||||||
Ok(info) => info.all_on(),
|
|
||||||
Err(err) => {
|
|
||||||
error!(id = this.get_id(), "Failed to parse message: {err}");
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(on);
|
|
||||||
}
|
|
||||||
Err(err) => error!(id = this.get_id(), "Error: {err}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn definitions() -> Option<String> {
|
|
||||||
Some(format!(
|
|
||||||
"---@async\n---@return boolean\nfunction {}:all_on() end\n",
|
|
||||||
<HueGroup as Typed>::type_name(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod message {
|
mod message {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -207,9 +164,5 @@ mod message {
|
|||||||
pub fn any_on(&self) -> bool {
|
pub fn any_on(&self) -> bool {
|
||||||
self.state.any_on
|
self.state.any_on
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_on(&self) -> bool {
|
|
||||||
self.state.all_on
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,10 +98,7 @@ impl LuaDeviceCreate for HueSwitch {
|
|||||||
impl OnMqtt for HueSwitch {
|
impl OnMqtt for HueSwitch {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the device itself or from a remote
|
// Check if the message is from the device itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let message = match serde_json::from_slice::<State>(&message.payload) {
|
let message = match serde_json::from_slice::<State>(&message.payload) {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -125,11 +122,15 @@ impl OnMqtt for HueSwitch {
|
|||||||
Action::LeftHold => self.config.left_hold_callback.call(self.clone()).await,
|
Action::LeftHold => self.config.left_hold_callback.call(self.clone()).await,
|
||||||
Action::RightHold => self.config.right_hold_callback.call(self.clone()).await,
|
Action::RightHold => self.config.right_hold_callback.call(self.clone()).await,
|
||||||
// If there is no hold action, the switch will act like a normal release
|
// If there is no hold action, the switch will act like a normal release
|
||||||
Action::RightHoldRelease if self.config.right_hold_callback.is_empty() => {
|
Action::RightHoldRelease => {
|
||||||
self.config.right_callback.call(self.clone()).await
|
if self.config.right_hold_callback.is_empty() {
|
||||||
|
self.config.right_callback.call(self.clone()).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Action::LeftHoldRelease if self.config.left_hold_callback.is_empty() => {
|
Action::LeftHoldRelease => {
|
||||||
self.config.left_callback.call(self.clone()).await
|
if self.config.left_hold_callback.is_empty() {
|
||||||
|
self.config.left_callback.call(self.clone()).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ impl LuaDeviceCreate for IkeaRemote {
|
|||||||
impl OnMqtt for IkeaRemote {
|
impl OnMqtt for IkeaRemote {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the deviec itself or from a remote
|
// Check if the message is from the deviec itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let message = match RemoteMessage::try_from(message) {
|
let message = match RemoteMessage::try_from(message) {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#![feature(debug_closure_helpers)]
|
|
||||||
#![feature(iter_intersperse)]
|
#![feature(iter_intersperse)]
|
||||||
mod air_filter;
|
mod air_filter;
|
||||||
mod bambu;
|
|
||||||
mod contact_sensor;
|
mod contact_sensor;
|
||||||
mod hue_bridge;
|
mod hue_bridge;
|
||||||
mod hue_group;
|
mod hue_group;
|
||||||
@@ -15,9 +13,6 @@ mod wake_on_lan;
|
|||||||
mod washer;
|
mod washer;
|
||||||
mod zigbee;
|
mod zigbee;
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
use automation_lib::Module;
|
use automation_lib::Module;
|
||||||
use automation_lib::device::{Device, LuaDeviceCreate};
|
use automation_lib::device::{Device, LuaDeviceCreate};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -25,31 +20,6 @@ use tracing::{debug, warn};
|
|||||||
type DeviceNameFn = fn() -> String;
|
type DeviceNameFn = fn() -> String;
|
||||||
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
|
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct DebugWrap<T: Clone>(T);
|
|
||||||
|
|
||||||
impl<T: Clone> DerefMut for DebugWrap<T> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> Deref for DebugWrap<T> {
|
|
||||||
type Target = T;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> fmt::Debug for DebugWrap<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_tuple("DebugWrap")
|
|
||||||
.field_with(|f| f.write_str(stringify!(T)))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegisteredDevice {
|
pub struct RegisteredDevice {
|
||||||
name_fn: DeviceNameFn,
|
name_fn: DeviceNameFn,
|
||||||
register_fn: RegisterDeviceFn,
|
register_fn: RegisterDeviceFn,
|
||||||
|
|||||||
@@ -85,10 +85,7 @@ impl Device for LightSensor {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for LightSensor {
|
impl OnMqtt for LightSensor {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
if !rumqttc::matches(
|
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,10 +105,7 @@ impl Device for Presence {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for Presence {
|
impl OnMqtt for Presence {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
if !rumqttc::matches(
|
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +116,7 @@ impl OnMqtt for Presence {
|
|||||||
.find('+')
|
.find('+')
|
||||||
.or(self.config.mqtt.topic.find('#'))
|
.or(self.config.mqtt.topic.find('#'))
|
||||||
.expect("Presence::create fails if it does not contain wildcards");
|
.expect("Presence::create fails if it does not contain wildcards");
|
||||||
let device_name: String = str::from_utf8(&message.topic[offset..])
|
let device_name = message.topic[offset..].into();
|
||||||
.expect("Topic should be valid")
|
|
||||||
.into();
|
|
||||||
|
|
||||||
if message.payload.is_empty() {
|
if message.payload.is_empty() {
|
||||||
// Remove the device from the map
|
// Remove the device from the map
|
||||||
|
|||||||
@@ -66,10 +66,7 @@ impl Device for WakeOnLAN {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for WakeOnLAN {
|
impl OnMqtt for WakeOnLAN {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
if !rumqttc::matches(
|
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,10 +89,7 @@ const HYSTERESIS: isize = 10;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for Washer {
|
impl OnMqtt for Washer {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
if !rumqttc::matches(
|
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use core::str;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -17,7 +16,7 @@ use google_home::errors::ErrorCode;
|
|||||||
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
|
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
|
||||||
use google_home::types::Type;
|
use google_home::types::Type;
|
||||||
use lua_typed::Typed;
|
use lua_typed::Typed;
|
||||||
use rumqttc::{Publish, PublishOptions, matches};
|
use rumqttc::{Publish, matches};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
@@ -178,10 +177,7 @@ where
|
|||||||
impl OnMqtt for LightOnOff {
|
impl OnMqtt for LightOnOff {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the device itself or from a remote
|
// Check if the message is from the device itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let state = match serde_json::from_slice::<StateOnOff>(&message.payload) {
|
let state = match serde_json::from_slice::<StateOnOff>(&message.payload) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -214,10 +210,7 @@ impl OnMqtt for LightOnOff {
|
|||||||
impl OnMqtt for LightBrightness {
|
impl OnMqtt for LightBrightness {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the deviec itself or from a remote
|
// Check if the message is from the deviec itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let state = match serde_json::from_slice::<StateBrightness>(&message.payload) {
|
let state = match serde_json::from_slice::<StateBrightness>(&message.payload) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -256,10 +249,7 @@ impl OnMqtt for LightBrightness {
|
|||||||
impl OnMqtt for LightColorTemperature {
|
impl OnMqtt for LightColorTemperature {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the deviec itself or from a remote
|
// Check if the message is from the deviec itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let state = match serde_json::from_slice::<StateColorTemperature>(&message.payload) {
|
let state = match serde_json::from_slice::<StateColorTemperature>(&message.payload) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -352,8 +342,9 @@ where
|
|||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
&topic,
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
PublishOptions::at_least_once(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||||
@@ -395,8 +386,9 @@ where
|
|||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
&topic,
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
PublishOptions::at_least_once(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||||
@@ -442,8 +434,9 @@ where
|
|||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
&topic,
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
PublishOptions::at_least_once(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use google_home::errors::ErrorCode;
|
|||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
use google_home::types::Type;
|
use google_home::types::Type;
|
||||||
use lua_typed::Typed;
|
use lua_typed::Typed;
|
||||||
use rumqttc::{Publish, PublishOptions, matches};
|
use rumqttc::{Publish, matches};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
@@ -162,10 +162,7 @@ where
|
|||||||
impl OnMqtt for OutletOnOff {
|
impl OnMqtt for OutletOnOff {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the device itself or from a remote
|
// Check if the message is from the device itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let state = match serde_json::from_slice::<StateOnOff>(&message.payload) {
|
let state = match serde_json::from_slice::<StateOnOff>(&message.payload) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -198,10 +195,7 @@ impl OnMqtt for OutletOnOff {
|
|||||||
impl OnMqtt for OutletPower {
|
impl OnMqtt for OutletPower {
|
||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the deviec itself or from a remote
|
// Check if the message is from the deviec itself or from a remote
|
||||||
if matches(
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
str::from_utf8(&message.topic).expect("Topic should be valid"),
|
|
||||||
&self.config.mqtt.topic,
|
|
||||||
) {
|
|
||||||
let state = match serde_json::from_slice::<StatePower>(&message.payload) {
|
let state = match serde_json::from_slice::<StatePower>(&message.payload) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -290,8 +284,9 @@ where
|
|||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
&topic,
|
||||||
|
rumqttc::QoS::AtLeastOnce,
|
||||||
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
PublishOptions::at_least_once(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ hostname = { workspace = true }
|
|||||||
inventory = { workspace = true }
|
inventory = { workspace = true }
|
||||||
lua_typed = { workspace = true }
|
lua_typed = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
rumqttc-next = { workspace = true }
|
rumqttc = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use automation_macro::LuaDeviceConfig;
|
use automation_macro::LuaDeviceConfig;
|
||||||
use lua_typed::Typed;
|
use lua_typed::Typed;
|
||||||
use mlua::FromLua;
|
use mlua::FromLua;
|
||||||
use rumqttc::{AsyncClient, Event, Incoming, MqttOptions, PublishOptions, Transport};
|
use rumqttc::{AsyncClient, Event, Incoming, MqttOptions, Transport};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
@@ -23,9 +24,9 @@ pub struct MqttConfig {
|
|||||||
|
|
||||||
impl From<MqttConfig> for MqttOptions {
|
impl From<MqttConfig> for MqttOptions {
|
||||||
fn from(value: MqttConfig) -> Self {
|
fn from(value: MqttConfig) -> Self {
|
||||||
let mut mqtt_options = MqttOptions::new(value.client_name, (value.host, value.port));
|
let mut mqtt_options = MqttOptions::new(value.client_name, value.host, value.port);
|
||||||
mqtt_options.set_credentials(value.username, value.password);
|
mqtt_options.set_credentials(value.username, value.password);
|
||||||
mqtt_options.set_keep_alive(5);
|
mqtt_options.set_keep_alive(Duration::from_secs(5));
|
||||||
|
|
||||||
if value.tls {
|
if value.tls {
|
||||||
mqtt_options.set_transport(Transport::tls_with_default_config());
|
mqtt_options.set_transport(Transport::tls_with_default_config());
|
||||||
@@ -91,7 +92,7 @@ impl mlua::UserData for WrappedAsyncClient {
|
|||||||
debug!("message = {message}");
|
debug!("message = {message}");
|
||||||
|
|
||||||
this.0
|
this.0
|
||||||
.publish(topic, message, PublishOptions::at_least_once().retained())
|
.publish(topic, rumqttc::QoS::AtLeastOnce, true, message)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ impl mlua::UserData for WrappedAsyncClient {
|
|||||||
|
|
||||||
pub fn start(config: MqttConfig, event_channel: &EventChannel) -> WrappedAsyncClient {
|
pub fn start(config: MqttConfig, event_channel: &EventChannel) -> WrappedAsyncClient {
|
||||||
let tx = event_channel.get_tx();
|
let tx = event_channel.get_tx();
|
||||||
let (client, mut eventloop) = AsyncClient::builder(config.into()).capacity(100).build();
|
let (client, mut eventloop) = AsyncClient::new(config.into(), 100);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
debug!("Listening for MQTT events");
|
debug!("Listening for MQTT events");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![feature(iter_intersperse)]
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
mod device;
|
mod device;
|
||||||
mod lua_device_config;
|
mod lua_device_config;
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ fn field_from_lua(field: &Field) -> TokenStream {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|arg| match arg {
|
.filter_map(|arg| match arg {
|
||||||
Argument::Flatten { .. } => Some(quote! {
|
Argument::Flatten { .. } => Some(quote! {
|
||||||
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::serde::DeserializeOptions::new().deny_unsupported_types(false))?
|
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))?
|
||||||
}),
|
}),
|
||||||
Argument::FromLua { .. } => Some(quote! {
|
Argument::FromLua { .. } => Some(quote! {
|
||||||
if table.contains_key(#table_name)? {
|
if table.contains_key(#table_name)? {
|
||||||
|
|||||||
+1
-7
@@ -1,6 +1,5 @@
|
|||||||
local ntfy = require("config.ntfy")
|
local ntfy = require("config.ntfy")
|
||||||
|
|
||||||
--- @class BatteryModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @type {[string]: number}
|
--- @type {[string]: number}
|
||||||
@@ -18,7 +17,7 @@ function module.callback(device, battery)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function notify_low_battery()
|
function module.notify_low_battery()
|
||||||
-- Don't send notifications if there are now devices with low battery
|
-- Don't send notifications if there are now devices with low battery
|
||||||
if next(low_battery) == nil then
|
if next(low_battery) == nil then
|
||||||
print("No devices with low battery")
|
print("No devices with low battery")
|
||||||
@@ -39,9 +38,4 @@ local function notify_low_battery()
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type Schedule
|
|
||||||
module.schedule = {
|
|
||||||
["0 0 21 */1 * *"] = notify_low_battery,
|
|
||||||
}
|
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|||||||
+12
-2
@@ -10,12 +10,12 @@ return {
|
|||||||
openid_url = "https://login.huizinga.dev/api/oidc",
|
openid_url = "https://login.huizinga.dev/api/oidc",
|
||||||
},
|
},
|
||||||
mqtt = {
|
mqtt = {
|
||||||
host = ((host == "zeus" or host == "hephaestus") and "olympus.huizinga.lan") or "mosquitto",
|
host = ((host == "zeus" or host == "hephaestus") and "olympus.lan.huizinga.dev") or "mosquitto",
|
||||||
port = 8883,
|
port = 8883,
|
||||||
client_name = "automation-" .. host,
|
client_name = "automation-" .. host,
|
||||||
username = "mqtt",
|
username = "mqtt",
|
||||||
password = secrets.mqtt_password,
|
password = secrets.mqtt_password,
|
||||||
tls = false,
|
tls = host == "zeus" or host == "hephaestus",
|
||||||
},
|
},
|
||||||
modules = {
|
modules = {
|
||||||
require("config.battery"),
|
require("config.battery"),
|
||||||
@@ -29,4 +29,14 @@ return {
|
|||||||
require("config.rooms"),
|
require("config.rooms"),
|
||||||
require("config.windows"),
|
require("config.windows"),
|
||||||
},
|
},
|
||||||
|
-- TODO: Make this also part of the modules
|
||||||
|
schedule = {
|
||||||
|
["0 0 19 * * *"] = function()
|
||||||
|
require("config.rooms.bedroom").set_airfilter_on(true)
|
||||||
|
end,
|
||||||
|
["0 0 20 * * *"] = function()
|
||||||
|
require("config.rooms.bedroom").set_airfilter_on(false)
|
||||||
|
end,
|
||||||
|
["0 0 21 */1 * *"] = require("config.battery").notify_low_battery,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,6 @@ local presence = require("config.presence")
|
|||||||
local utils = require("automation:utils")
|
local utils = require("automation:utils")
|
||||||
local variables = require("automation:variables")
|
local variables = require("automation:variables")
|
||||||
|
|
||||||
--- @class DebugModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
if variables.debug == "true" then
|
if variables.debug == "true" then
|
||||||
@@ -17,6 +16,8 @@ end
|
|||||||
|
|
||||||
--- @type SetupFunction
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
|
print("Lua " .. _VERSION .. " running on " .. utils.get_hostname())
|
||||||
|
|
||||||
presence.add_callback(function(p)
|
presence.add_callback(function(p)
|
||||||
mqtt_client:send_message(helper.mqtt_automation("debug") .. "/presence", {
|
mqtt_client:send_message(helper.mqtt_automation("debug") .. "/presence", {
|
||||||
state = p,
|
state = p,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
local debug = require("config.debug")
|
local debug = require("config.debug")
|
||||||
local utils = require("automation:utils")
|
local utils = require("automation:utils")
|
||||||
|
|
||||||
--- @class HallwayAutomationModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
local timeout = utils.Timeout.new()
|
local timeout = utils.Timeout.new()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
local utils = require("automation:utils")
|
local utils = require("automation:utils")
|
||||||
|
|
||||||
--- @class HelperModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @param topic string
|
--- @param topic string
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ local light = require("config.light")
|
|||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
local secrets = require("automation:secrets")
|
local secrets = require("automation:secrets")
|
||||||
|
|
||||||
--- @class HueBridgeModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
module.ip = "10.0.0.102"
|
module.ip = "10.0.0.102"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
local devices = require("automation:devices")
|
local devices = require("automation:devices")
|
||||||
local helper = require("config.helper")
|
local helper = require("config.helper")
|
||||||
|
|
||||||
--- @class LightModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @class OnPresence
|
--- @class OnPresence
|
||||||
@@ -35,7 +34,6 @@ function module.setup(mqtt_client)
|
|||||||
callback = callback,
|
callback = callback,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
module.device,
|
module.device,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
local devices = require("automation:devices")
|
local devices = require("automation:devices")
|
||||||
local secrets = require("automation:secrets")
|
local secrets = require("automation:secrets")
|
||||||
|
|
||||||
--- @class NtfyModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
local ntfy_topic = secrets.ntfy_topic
|
local ntfy_topic = secrets.ntfy_topic
|
||||||
@@ -25,7 +24,6 @@ function module.setup()
|
|||||||
topic = ntfy_topic,
|
topic = ntfy_topic,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
ntfy,
|
ntfy,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ local devices = require("automation:devices")
|
|||||||
local helper = require("config.helper")
|
local helper = require("config.helper")
|
||||||
local ntfy = require("config.ntfy")
|
local ntfy = require("config.ntfy")
|
||||||
|
|
||||||
--- @class PresenceModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @class OnPresence
|
--- @class OnPresence
|
||||||
@@ -62,7 +61,6 @@ function module.setup(mqtt_client)
|
|||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
presence,
|
presence,
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
--- @type Module
|
--- @type SetupInner
|
||||||
return {
|
return {
|
||||||
require("config.rooms.bathroom"),
|
require("config.rooms.bathroom"),
|
||||||
require("config.rooms.bedroom"),
|
require("config.rooms.bedroom"),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ local devices = require("automation:devices")
|
|||||||
local helper = require("config.helper")
|
local helper = require("config.helper")
|
||||||
local ntfy = require("config.ntfy")
|
local ntfy = require("config.ntfy")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local light = devices.LightOnOff.new({
|
local light = devices.LightOnOff.new({
|
||||||
name = "Light",
|
name = "Light",
|
||||||
@@ -30,7 +30,6 @@ function module.setup(mqtt_client)
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
light,
|
light,
|
||||||
washer,
|
washer,
|
||||||
|
|||||||
+14
-49
@@ -4,12 +4,12 @@ local helper = require("config.helper")
|
|||||||
local hue_bridge = require("config.hue_bridge")
|
local hue_bridge = require("config.hue_bridge")
|
||||||
local windows = require("config.windows")
|
local windows = require("config.windows")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @type AirFilter?
|
--- @type AirFilter?
|
||||||
local air_filter = nil
|
local air_filter = nil
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local lights = devices.HueGroup.new({
|
local lights = devices.HueGroup.new({
|
||||||
identifier = "bedroom_lights",
|
identifier = "bedroom_lights",
|
||||||
@@ -25,13 +25,6 @@ function module.setup(mqtt_client)
|
|||||||
group_id = 3,
|
group_id = 3,
|
||||||
scene_id = "60tfTyR168v2csz",
|
scene_id = "60tfTyR168v2csz",
|
||||||
})
|
})
|
||||||
local wardrobe_light = devices.HueGroup.new({
|
|
||||||
identifier = "bedroom_lights_wardrobe",
|
|
||||||
ip = hue_bridge.ip,
|
|
||||||
login = hue_bridge.token,
|
|
||||||
group_id = 3,
|
|
||||||
scene_id = "1IDvpsN2YLZsDV95",
|
|
||||||
})
|
|
||||||
|
|
||||||
air_filter = devices.AirFilter.new({
|
air_filter = devices.AirFilter.new({
|
||||||
name = "Air Filter",
|
name = "Air Filter",
|
||||||
@@ -39,36 +32,13 @@ function module.setup(mqtt_client)
|
|||||||
url = "http://10.0.0.103",
|
url = "http://10.0.0.103",
|
||||||
})
|
})
|
||||||
|
|
||||||
local wardrobe_door = devices.ContactSensor.new({
|
|
||||||
name = "Wardrobe Door",
|
|
||||||
room = "Bedroom",
|
|
||||||
sensor_type = "Door",
|
|
||||||
topic = helper.mqtt_z2m("bedroom/wardrobe_door"),
|
|
||||||
client = mqtt_client,
|
|
||||||
callback = function(_, open)
|
|
||||||
-- Technically this has an edge case where if one of the spots is
|
|
||||||
-- on, but that is not something I ever do
|
|
||||||
if not lights:all_on() then
|
|
||||||
wardrobe_light:set_on(open)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
battery_callback = battery.callback,
|
|
||||||
})
|
|
||||||
|
|
||||||
local switch = devices.HueSwitch.new({
|
local switch = devices.HueSwitch.new({
|
||||||
name = "Switch",
|
name = "Switch",
|
||||||
room = "Bedroom",
|
room = "Bedroom",
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
topic = helper.mqtt_z2m("bedroom/switch"),
|
topic = helper.mqtt_z2m("bedroom/switch"),
|
||||||
left_callback = function()
|
left_callback = function()
|
||||||
local on = not lights:all_on()
|
lights:set_on(not lights:on())
|
||||||
lights:set_on(on)
|
|
||||||
-- This is a bit janky as the light will start to dim before turning
|
|
||||||
-- back on, however this is really and edge case that probably won't
|
|
||||||
-- happen often, so for now it's fine
|
|
||||||
if not on and wardrobe_door:open_percent() == 100 then
|
|
||||||
wardrobe_light:set_on(true)
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
left_hold_callback = function()
|
left_hold_callback = function()
|
||||||
lights_relax:set_on(true)
|
lights_relax:set_on(true)
|
||||||
@@ -85,25 +55,20 @@ function module.setup(mqtt_client)
|
|||||||
})
|
})
|
||||||
windows.add(window)
|
windows.add(window)
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
devices = {
|
lights,
|
||||||
lights,
|
lights_relax,
|
||||||
lights_relax,
|
air_filter,
|
||||||
air_filter,
|
switch,
|
||||||
wardrobe_door,
|
window,
|
||||||
switch,
|
|
||||||
window,
|
|
||||||
},
|
|
||||||
schedule = {
|
|
||||||
["0 0 19 * * *"] = function()
|
|
||||||
air_filter:set_on(true)
|
|
||||||
end,
|
|
||||||
["0 0 20 * * *"] = function()
|
|
||||||
air_filter:set_on(false)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param on boolean
|
||||||
|
function module.set_airfilter_on(on)
|
||||||
|
if air_filter then
|
||||||
|
air_filter:set_on(on)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|||||||
@@ -4,35 +4,15 @@ local helper = require("config.helper")
|
|||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
local windows = require("config.windows")
|
local windows = require("config.windows")
|
||||||
|
|
||||||
local secrets = require("automation:secrets")
|
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local light = nil
|
local light = devices.LightOnOff.new({
|
||||||
|
|
||||||
local bambu = devices.Bambu.new({
|
|
||||||
host = "10.0.0.108",
|
|
||||||
device_id = secrets.printer_device_id,
|
|
||||||
access_code = secrets.printer_access_code,
|
|
||||||
callbacks = {
|
|
||||||
connected = function(self)
|
|
||||||
if light ~= nil then
|
|
||||||
self:set_on(light:on())
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
light = devices.LightOnOff.new({
|
|
||||||
name = "Light",
|
name = "Light",
|
||||||
room = "Guest Room",
|
room = "Guest Room",
|
||||||
topic = helper.mqtt_z2m("guest/light"),
|
topic = helper.mqtt_z2m("guest/light"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
callback = function(_, state)
|
|
||||||
bambu:set_on(state.state)
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
presence.turn_off_when_away(light)
|
presence.turn_off_when_away(light)
|
||||||
|
|
||||||
@@ -45,19 +25,9 @@ function module.setup(mqtt_client)
|
|||||||
})
|
})
|
||||||
windows.add(window)
|
windows.add(window)
|
||||||
|
|
||||||
local printer = devices.OutletOnOff.new({
|
|
||||||
name = "3D Printer",
|
|
||||||
room = "Guest Room",
|
|
||||||
topic = helper.mqtt_z2m("guest/printer"),
|
|
||||||
client = mqtt_client,
|
|
||||||
})
|
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
light,
|
light,
|
||||||
window,
|
window,
|
||||||
printer,
|
|
||||||
bambu,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ local presence = require("config.presence")
|
|||||||
local utils = require("automation:utils")
|
local utils = require("automation:utils")
|
||||||
local windows = require("config.windows")
|
local windows = require("config.windows")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local main_light = devices.HueGroup.new({
|
local main_light = devices.HueGroup.new({
|
||||||
identifier = "hallway_main_light",
|
identifier = "hallway_main_light",
|
||||||
@@ -59,21 +59,26 @@ function module.setup(mqtt_client)
|
|||||||
})
|
})
|
||||||
hallway_automation.set_trash(trash)
|
hallway_automation.set_trash(trash)
|
||||||
|
|
||||||
local timeout = utils.Timeout.new()
|
---@param duration number
|
||||||
local function frontdoor_presence(_, open)
|
---@return fun(_, open: boolean)
|
||||||
if open then
|
local function frontdoor_presence(duration)
|
||||||
timeout:cancel()
|
local timeout = utils.Timeout.new()
|
||||||
|
|
||||||
if not presence.overall_presence() then
|
return function(_, open)
|
||||||
mqtt_client:send_message(helper.mqtt_automation("presence/contact/frontdoor"), {
|
if open then
|
||||||
state = true,
|
timeout:cancel()
|
||||||
updated = utils.get_epoch(),
|
|
||||||
})
|
if presence.overall_presence() then
|
||||||
|
mqtt_client:send_message(helper.mqtt_automation("presence/contact/frontdoor"), {
|
||||||
|
state = true,
|
||||||
|
updated = utils.get_epoch(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
timeout:start(duration, function()
|
||||||
|
mqtt_client:send_message(helper.mqtt_automation("presence/contact/frontdoor"), nil)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
timeout:start(debug.debug_mode and 10 or 15 * 60, function()
|
|
||||||
mqtt_client:send_message(helper.mqtt_automation("presence/contact/frontdoor"), nil)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ function module.setup(mqtt_client)
|
|||||||
topic = helper.mqtt_z2m("hallway/frontdoor"),
|
topic = helper.mqtt_z2m("hallway/frontdoor"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
callback = {
|
callback = {
|
||||||
frontdoor_presence,
|
frontdoor_presence(debug.debug_mode and 10 or 15 * 60),
|
||||||
hallway_automation.door_callback,
|
hallway_automation.door_callback,
|
||||||
},
|
},
|
||||||
battery_callback = battery.callback,
|
battery_callback = battery.callback,
|
||||||
@@ -92,7 +97,6 @@ function module.setup(mqtt_client)
|
|||||||
windows.add(frontdoor)
|
windows.add(frontdoor)
|
||||||
hallway_automation.set_door(frontdoor)
|
hallway_automation.set_door(frontdoor)
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
main_light,
|
main_light,
|
||||||
storage_light,
|
storage_light,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ local devices = require("automation:devices")
|
|||||||
local helper = require("config.helper")
|
local helper = require("config.helper")
|
||||||
local hue_bridge = require("config.hue_bridge")
|
local hue_bridge = require("config.hue_bridge")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local light = devices.HueGroup.new({
|
local light = devices.HueGroup.new({
|
||||||
identifier = "hallway_top_light",
|
identifier = "hallway_top_light",
|
||||||
@@ -37,7 +37,6 @@ function module.setup(mqtt_client)
|
|||||||
battery_callback = battery.callback,
|
battery_callback = battery.callback,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
light,
|
light,
|
||||||
top_switch,
|
top_switch,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ local helper = require("config.helper")
|
|||||||
local hue_bridge = require("config.hue_bridge")
|
local hue_bridge = require("config.hue_bridge")
|
||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
|
|
||||||
--- @class KitchenModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @type HueGroup?
|
--- @type HueGroup?
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ local hue_bridge = require("config.hue_bridge")
|
|||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
local windows = require("config.windows")
|
local windows = require("config.windows")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local lights = devices.HueGroup.new({
|
local lights = devices.HueGroup.new({
|
||||||
identifier = "living_lights",
|
identifier = "living_lights",
|
||||||
@@ -109,7 +109,6 @@ function module.setup(mqtt_client)
|
|||||||
})
|
})
|
||||||
windows.add(window)
|
windows.add(window)
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
lights,
|
lights,
|
||||||
lights_relax,
|
lights_relax,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ local devices = require("automation:devices")
|
|||||||
local helper = require("config.helper")
|
local helper = require("config.helper")
|
||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local light = devices.LightBrightness.new({
|
local light = devices.LightBrightness.new({
|
||||||
name = "Light",
|
name = "Light",
|
||||||
@@ -31,7 +31,6 @@ function module.setup(mqtt_client)
|
|||||||
battery_callback = battery.callback,
|
battery_callback = battery.callback,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
light,
|
light,
|
||||||
door,
|
door,
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ local helper = require("config.helper")
|
|||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
local utils = require("automation:utils")
|
local utils = require("automation:utils")
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
|
--- @type SetupFunction
|
||||||
function module.setup(mqtt_client)
|
function module.setup(mqtt_client)
|
||||||
local charger = devices.OutletOnOff.new({
|
local charger = devices.OutletOnOff.new({
|
||||||
name = "Charger",
|
name = "Charger",
|
||||||
@@ -57,7 +57,6 @@ function module.setup(mqtt_client)
|
|||||||
battery_callback = battery.callback,
|
battery_callback = battery.callback,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @type Module
|
|
||||||
return {
|
return {
|
||||||
charger,
|
charger,
|
||||||
outlets,
|
outlets,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
local ntfy = require("config.ntfy")
|
local ntfy = require("config.ntfy")
|
||||||
local presence = require("config.presence")
|
local presence = require("config.presence")
|
||||||
|
|
||||||
--- @class WindowsModule: Module
|
|
||||||
local module = {}
|
local module = {}
|
||||||
|
|
||||||
--- @class OnPresence
|
--- @class OnPresence
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ local devices
|
|||||||
---@class Action
|
---@class Action
|
||||||
---@field action
|
---@field action
|
||||||
---| "broadcast"
|
---| "broadcast"
|
||||||
---@field extras (table<string, string>)?
|
---@field extras table<string, string>?
|
||||||
---@field label string
|
---@field label string
|
||||||
---@field clear (boolean)?
|
---@field clear boolean?
|
||||||
local Action
|
local Action
|
||||||
|
|
||||||
---@class AirFilter: DeviceInterface, OnOffInterface
|
---@class AirFilter: DeviceInterface, OnOffInterface
|
||||||
@@ -20,68 +20,49 @@ function devices.AirFilter.new(config) end
|
|||||||
|
|
||||||
---@class AirFilterConfig
|
---@class AirFilterConfig
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field url string
|
---@field url string
|
||||||
local AirFilterConfig
|
local AirFilterConfig
|
||||||
|
|
||||||
---@class Bambu: DeviceInterface, OnOffInterface
|
|
||||||
local Bambu
|
|
||||||
devices.Bambu = {}
|
|
||||||
---@param config BambuConfig
|
|
||||||
---@return Bambu
|
|
||||||
function devices.Bambu.new(config) end
|
|
||||||
|
|
||||||
---@class BambuCallbacks
|
|
||||||
---@field state (fun(_: Bambu) | fun(_: Bambu)[])?
|
|
||||||
---@field connected (fun(_: Bambu) | fun(_: Bambu)[])?
|
|
||||||
local BambuCallbacks
|
|
||||||
|
|
||||||
---@class BambuConfig
|
|
||||||
---@field host string
|
|
||||||
---@field device_id string
|
|
||||||
---@field access_code string
|
|
||||||
---@field callbacks BambuCallbacks
|
|
||||||
local BambuConfig
|
|
||||||
|
|
||||||
---@class ConfigLightLightStateBrightness
|
---@class ConfigLightLightStateBrightness
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field callback (fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[])?
|
---@field callback fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[]?
|
||||||
---@field client (AsyncClient)?
|
---@field client AsyncClient?
|
||||||
local ConfigLightLightStateBrightness
|
local ConfigLightLightStateBrightness
|
||||||
|
|
||||||
---@class ConfigLightLightStateColorTemperature
|
---@class ConfigLightLightStateColorTemperature
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field callback (fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[])?
|
---@field callback fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[]?
|
||||||
---@field client (AsyncClient)?
|
---@field client AsyncClient?
|
||||||
local ConfigLightLightStateColorTemperature
|
local ConfigLightLightStateColorTemperature
|
||||||
|
|
||||||
---@class ConfigLightLightStateOnOff
|
---@class ConfigLightLightStateOnOff
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field callback (fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[])?
|
---@field callback fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[]?
|
||||||
---@field client (AsyncClient)?
|
---@field client AsyncClient?
|
||||||
local ConfigLightLightStateOnOff
|
local ConfigLightLightStateOnOff
|
||||||
|
|
||||||
---@class ConfigOutletOutletStateOnOff
|
---@class ConfigOutletOutletStateOnOff
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field outlet_type (OutletType)?
|
---@field outlet_type OutletType?
|
||||||
---@field callback (fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[])?
|
---@field callback fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[]?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local ConfigOutletOutletStateOnOff
|
local ConfigOutletOutletStateOnOff
|
||||||
|
|
||||||
---@class ConfigOutletOutletStatePower
|
---@class ConfigOutletOutletStatePower
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field outlet_type (OutletType)?
|
---@field outlet_type OutletType?
|
||||||
---@field callback (fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[])?
|
---@field callback fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[]?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local ConfigOutletOutletStatePower
|
local ConfigOutletOutletStatePower
|
||||||
|
|
||||||
@@ -94,12 +75,12 @@ function devices.ContactSensor.new(config) end
|
|||||||
|
|
||||||
---@class ContactSensorConfig
|
---@class ContactSensorConfig
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field sensor_type (SensorType)?
|
---@field sensor_type SensorType?
|
||||||
---@field callback (fun(_: ContactSensor, _: boolean) | fun(_: ContactSensor, _: boolean)[])?
|
---@field callback fun(_: ContactSensor, _: boolean) | fun(_: ContactSensor, _: boolean)[]?
|
||||||
---@field battery_callback (fun(_: ContactSensor, _: number) | fun(_: ContactSensor, _: number)[])?
|
---@field battery_callback fun(_: ContactSensor, _: number) | fun(_: ContactSensor, _: number)[]?
|
||||||
---@field client (AsyncClient)?
|
---@field client AsyncClient?
|
||||||
local ContactSensorConfig
|
local ContactSensorConfig
|
||||||
|
|
||||||
---@alias Flag
|
---@alias Flag
|
||||||
@@ -135,9 +116,6 @@ devices.HueGroup = {}
|
|||||||
---@param config HueGroupConfig
|
---@param config HueGroupConfig
|
||||||
---@return HueGroup
|
---@return HueGroup
|
||||||
function devices.HueGroup.new(config) end
|
function devices.HueGroup.new(config) end
|
||||||
---@async
|
|
||||||
---@return boolean
|
|
||||||
function HueGroup:all_on() end
|
|
||||||
|
|
||||||
---@class HueGroupConfig
|
---@class HueGroupConfig
|
||||||
---@field identifier string
|
---@field identifier string
|
||||||
@@ -156,14 +134,14 @@ function devices.HueSwitch.new(config) end
|
|||||||
|
|
||||||
---@class HueSwitchConfig
|
---@class HueSwitchConfig
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
---@field left_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
|
---@field left_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
|
||||||
---@field right_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
|
---@field right_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
|
||||||
---@field left_hold_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
|
---@field left_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
|
||||||
---@field right_hold_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
|
---@field right_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
|
||||||
---@field battery_callback (fun(_: HueSwitch, _: number) | fun(_: HueSwitch, _: number)[])?
|
---@field battery_callback fun(_: HueSwitch, _: number) | fun(_: HueSwitch, _: number)[]?
|
||||||
local HueSwitchConfig
|
local HueSwitchConfig
|
||||||
|
|
||||||
---@class IkeaRemote: DeviceInterface
|
---@class IkeaRemote: DeviceInterface
|
||||||
@@ -175,12 +153,12 @@ function devices.IkeaRemote.new(config) end
|
|||||||
|
|
||||||
---@class IkeaRemoteConfig
|
---@class IkeaRemoteConfig
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field single_button (boolean)?
|
---@field single_button boolean?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
---@field callback (fun(_: IkeaRemote, _: boolean) | fun(_: IkeaRemote, _: boolean)[])?
|
---@field callback fun(_: IkeaRemote, _: boolean) | fun(_: IkeaRemote, _: boolean)[]?
|
||||||
---@field battery_callback (fun(_: IkeaRemote, _: number) | fun(_: IkeaRemote, _: number)[])?
|
---@field battery_callback fun(_: IkeaRemote, _: number) | fun(_: IkeaRemote, _: number)[]?
|
||||||
local IkeaRemoteConfig
|
local IkeaRemoteConfig
|
||||||
|
|
||||||
---@class KasaOutlet: DeviceInterface, OnOffInterface
|
---@class KasaOutlet: DeviceInterface, OnOffInterface
|
||||||
@@ -228,7 +206,7 @@ function devices.LightSensor.new(config) end
|
|||||||
---@field topic string
|
---@field topic string
|
||||||
---@field min integer
|
---@field min integer
|
||||||
---@field max integer
|
---@field max integer
|
||||||
---@field callback (fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[])?
|
---@field callback fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[]?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local LightSensorConfig
|
local LightSensorConfig
|
||||||
|
|
||||||
@@ -249,10 +227,10 @@ local LightStateOnOff
|
|||||||
|
|
||||||
---@class Notification
|
---@class Notification
|
||||||
---@field title string
|
---@field title string
|
||||||
---@field message (string)?
|
---@field message string?
|
||||||
---@field tags ((string)[])?
|
---@field tags string[]?
|
||||||
---@field priority (Priority)?
|
---@field priority Priority?
|
||||||
---@field actions ((Action)[])?
|
---@field actions Action[]?
|
||||||
local Notification
|
local Notification
|
||||||
|
|
||||||
---@class Ntfy: DeviceInterface
|
---@class Ntfy: DeviceInterface
|
||||||
@@ -266,7 +244,7 @@ function devices.Ntfy.new(config) end
|
|||||||
function Ntfy:send_notification(notification) end
|
function Ntfy:send_notification(notification) end
|
||||||
|
|
||||||
---@class NtfyConfig
|
---@class NtfyConfig
|
||||||
---@field url (string)?
|
---@field url string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
local NtfyConfig
|
local NtfyConfig
|
||||||
|
|
||||||
@@ -309,7 +287,7 @@ function Presence:overall_presence() end
|
|||||||
|
|
||||||
---@class PresenceConfig
|
---@class PresenceConfig
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field callback (fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[])?
|
---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local PresenceConfig
|
local PresenceConfig
|
||||||
|
|
||||||
@@ -343,16 +321,16 @@ function devices.Washer.new(config) end
|
|||||||
---@field identifier string
|
---@field identifier string
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field threshold number
|
---@field threshold number
|
||||||
---@field done_callback (fun(_: Washer) | fun(_: Washer)[])?
|
---@field done_callback fun(_: Washer) | fun(_: Washer)[]?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local WasherConfig
|
local WasherConfig
|
||||||
|
|
||||||
---@class WolConfig
|
---@class WolConfig
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field room (string)?
|
---@field room string?
|
||||||
---@field topic string
|
---@field topic string
|
||||||
---@field mac_address string
|
---@field mac_address string
|
||||||
---@field broadcast_ip (string)?
|
---@field broadcast_ip string?
|
||||||
---@field client AsyncClient
|
---@field client AsyncClient
|
||||||
local WolConfig
|
local WolConfig
|
||||||
|
|
||||||
|
|||||||
+8
-14
@@ -3,26 +3,20 @@
|
|||||||
|
|
||||||
---@class FulfillmentConfig
|
---@class FulfillmentConfig
|
||||||
---@field openid_url string
|
---@field openid_url string
|
||||||
---@field ip (string)?
|
---@field ip string?
|
||||||
---@field port (integer)?
|
---@field port integer?
|
||||||
local FulfillmentConfig
|
local FulfillmentConfig
|
||||||
|
|
||||||
---@class Config
|
---@class Config
|
||||||
---@field fulfillment FulfillmentConfig
|
---@field fulfillment FulfillmentConfig
|
||||||
---@field modules (Module)[]
|
---@field modules Setup?
|
||||||
---@field mqtt MqttConfig
|
---@field mqtt MqttConfig
|
||||||
|
---@field schedule table<string, fun() | fun()[]>?
|
||||||
local Config
|
local Config
|
||||||
|
|
||||||
---@alias SetupFunction fun(mqtt_client: AsyncClient): Module | DeviceInterface[] | nil
|
---@alias SetupFunction fun(mqtt_client: AsyncClient): SetupInner?
|
||||||
|
---@alias SetupInner (DeviceInterface | { setup: SetupFunction } | SetupInner)[]
|
||||||
---@alias Schedule table<string, fun() | fun()[]>
|
---@alias Setup SetupFunction | SetupInner
|
||||||
|
|
||||||
---@class Module
|
|
||||||
---@field setup (SetupFunction)?
|
|
||||||
---@field devices (DeviceInterface)[]?
|
|
||||||
---@field schedule Schedule?
|
|
||||||
---@field [number] (Module)[]?
|
|
||||||
local Module
|
|
||||||
|
|
||||||
---@class MqttConfig
|
---@class MqttConfig
|
||||||
---@field host string
|
---@field host string
|
||||||
@@ -30,7 +24,7 @@ local Module
|
|||||||
---@field client_name string
|
---@field client_name string
|
||||||
---@field username string
|
---@field username string
|
||||||
---@field password string
|
---@field password string
|
||||||
---@field tls (boolean)?
|
---@field tls boolean?
|
||||||
local MqttConfig
|
local MqttConfig
|
||||||
|
|
||||||
---@class AsyncClient
|
---@class AsyncClient
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
variable "TAG_BASE" {}
|
|
||||||
variable "RELEASE_VERSION" {}
|
|
||||||
|
|
||||||
group "default" {
|
|
||||||
targets = ["automation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "docker-metadata-action" {
|
|
||||||
tags = []
|
|
||||||
}
|
|
||||||
|
|
||||||
target "automation" {
|
|
||||||
inherits = ["docker-metadata-action"]
|
|
||||||
context = "./"
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
tags = [for tag in target.docker-metadata-action.tags : "${TAG_BASE}:${tag}"]
|
|
||||||
args = {
|
|
||||||
RELEASE_VERSION="${RELEASE_VERSION}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly-2026-05-12"
|
channel = "nightly-2025-08-20"
|
||||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::process;
|
|||||||
|
|
||||||
use ::config::{Environment, File};
|
use ::config::{Environment, File};
|
||||||
use automation::config::{Config, Setup};
|
use automation::config::{Config, Setup};
|
||||||
|
use automation::schedule::start_scheduler;
|
||||||
use automation::secret::EnvironmentSecretFile;
|
use automation::secret::EnvironmentSecretFile;
|
||||||
use automation::version::VERSION;
|
use automation::version::VERSION;
|
||||||
use automation::web::{ApiError, User};
|
use automation::web::{ApiError, User};
|
||||||
@@ -139,12 +140,13 @@ async fn app() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let mqtt_client = mqtt::start(config.mqtt, &device_manager.event_channel());
|
let mqtt_client = mqtt::start(config.mqtt, &device_manager.event_channel());
|
||||||
|
|
||||||
let resolved = config.modules.resolve(&lua, &mqtt_client).await?;
|
if let Some(modules) = config.modules {
|
||||||
for device in resolved.devices {
|
for device in modules.setup(&lua, &mqtt_client).await? {
|
||||||
device_manager.add(device).await;
|
device_manager.add(device).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved.scheduler.start().await?;
|
start_scheduler(config.schedule).await?;
|
||||||
|
|
||||||
// Create google home fulfillment route
|
// Create google home fulfillment route
|
||||||
let fulfillment = Router::new().route("/google_home", post(fulfillment));
|
let fulfillment = Router::new().route("/google_home", post(fulfillment));
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use automation::config::generate_definitions;
|
use automation::config::{Config, FulfillmentConfig, Setups};
|
||||||
use automation_lib::Module;
|
use automation_lib::Module;
|
||||||
|
use automation_lib::mqtt::{MqttConfig, WrappedAsyncClient};
|
||||||
|
use lua_typed::Typed;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
extern crate automation_devices;
|
extern crate automation_devices;
|
||||||
@@ -25,6 +27,24 @@ fn write_definitions(filename: &str, definitions: &str) -> std::io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config_definitions() -> String {
|
||||||
|
let mut output = "---@meta\n\n".to_string();
|
||||||
|
|
||||||
|
output +=
|
||||||
|
&FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition");
|
||||||
|
output += "\n";
|
||||||
|
output += &Config::generate_full().expect("Config should have a definition");
|
||||||
|
output += "\n";
|
||||||
|
output += &Setups::generate_full().expect("Setups should have a definition");
|
||||||
|
output += "\n";
|
||||||
|
output += &MqttConfig::generate_full().expect("MqttConfig should have a definition");
|
||||||
|
output += "\n";
|
||||||
|
output +=
|
||||||
|
&WrappedAsyncClient::generate_full().expect("WrappedAsyncClient should have a definition");
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
@@ -39,7 +59,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write_definitions("config.lua", &generate_definitions())?;
|
write_definitions("config.lua", &config_definitions())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-211
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use automation_lib::action_callback::ActionCallback;
|
use automation_lib::action_callback::ActionCallback;
|
||||||
use automation_lib::device::Device;
|
use automation_lib::device::Device;
|
||||||
@@ -10,8 +9,6 @@ use lua_typed::Typed;
|
|||||||
use mlua::FromLua;
|
use mlua::FromLua;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::schedule::Scheduler;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Setup {
|
pub struct Setup {
|
||||||
#[serde(default = "default_entrypoint")]
|
#[serde(default = "default_entrypoint")]
|
||||||
@@ -37,219 +34,90 @@ pub struct FulfillmentConfig {
|
|||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct SetupFunction(mlua::Function);
|
|
||||||
|
|
||||||
impl Typed for SetupFunction {
|
|
||||||
fn type_name() -> String {
|
|
||||||
"SetupFunction".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_header() -> Option<String> {
|
|
||||||
Some(format!(
|
|
||||||
"---@alias {} fun(mqtt_client: {}): {} | DeviceInterface[] | nil\n",
|
|
||||||
Self::type_name(),
|
|
||||||
WrappedAsyncClient::type_name(),
|
|
||||||
Module::type_name()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for SetupFunction {
|
|
||||||
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
|
|
||||||
Ok(Self(FromLua::from_lua(value, lua)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SetupFunction {
|
|
||||||
type Target = mlua::Function;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct Schedule(HashMap<String, ActionCallback<()>>);
|
pub struct Setups(mlua::Value);
|
||||||
|
|
||||||
impl Typed for Schedule {
|
impl Setups {
|
||||||
fn type_name() -> String {
|
pub async fn setup(
|
||||||
"Schedule".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_header() -> Option<String> {
|
|
||||||
Some(format!(
|
|
||||||
"---@alias {} {}\n",
|
|
||||||
Self::type_name(),
|
|
||||||
HashMap::<String, ActionCallback<()>>::type_name(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for Schedule {
|
|
||||||
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
|
|
||||||
Ok(Self(FromLua::from_lua(value, lua)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for Schedule {
|
|
||||||
type Item = <HashMap<String, ActionCallback<()>> as IntoIterator>::Item;
|
|
||||||
|
|
||||||
type IntoIter = <HashMap<String, ActionCallback<()>> as IntoIterator>::IntoIter;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct Module {
|
|
||||||
setup: Option<SetupFunction>,
|
|
||||||
devices: Vec<Box<dyn Device>>,
|
|
||||||
schedule: Schedule,
|
|
||||||
modules: Vec<Module>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add option to typed to rename field
|
|
||||||
impl Typed for Module {
|
|
||||||
fn type_name() -> String {
|
|
||||||
"Module".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_header() -> Option<String> {
|
|
||||||
Some(format!("---@class {}\n", Self::type_name()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_members() -> Option<String> {
|
|
||||||
Some(format!(
|
|
||||||
r#"---@field setup {}
|
|
||||||
---@field devices {}?
|
|
||||||
---@field schedule {}?
|
|
||||||
---@field [number] {}?
|
|
||||||
"#,
|
|
||||||
Option::<SetupFunction>::type_name(),
|
|
||||||
Vec::<Box<dyn Device>>::type_name(),
|
|
||||||
Schedule::type_name(),
|
|
||||||
Vec::<Module>::type_name(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_footer() -> Option<String> {
|
|
||||||
let type_name = <Self as Typed>::type_name();
|
|
||||||
Some(format!("local {type_name}\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for Module {
|
|
||||||
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
|
|
||||||
// When calling require it might return a result from the searcher indicating how the
|
|
||||||
// module was found, we want to ignore these entries.
|
|
||||||
// TODO: Find a better solution for this
|
|
||||||
if value.is_string() {
|
|
||||||
return Ok(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mlua::Value::Table(table) = value else {
|
|
||||||
return Err(mlua::Error::runtime(format!(
|
|
||||||
"Expected module table, instead found: {}",
|
|
||||||
value.type_name()
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let setup = table.get("setup")?;
|
|
||||||
let devices = table.get("devices").unwrap_or_default();
|
|
||||||
let schedule = table.get("schedule").unwrap_or_default();
|
|
||||||
|
|
||||||
let mut modules = Vec::new();
|
|
||||||
|
|
||||||
for module in table.sequence_values::<Module>() {
|
|
||||||
modules.push(module?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Module {
|
|
||||||
setup,
|
|
||||||
devices,
|
|
||||||
schedule,
|
|
||||||
modules,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Modules(Vec<Module>);
|
|
||||||
|
|
||||||
impl Typed for Modules {
|
|
||||||
fn type_name() -> String {
|
|
||||||
Vec::<Module>::type_name()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for Modules {
|
|
||||||
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
|
|
||||||
Ok(Self(FromLua::from_lua(value, lua)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Modules {
|
|
||||||
pub async fn resolve(
|
|
||||||
self,
|
self,
|
||||||
lua: &mlua::Lua,
|
lua: &mlua::Lua,
|
||||||
client: &WrappedAsyncClient,
|
client: &WrappedAsyncClient,
|
||||||
) -> mlua::Result<Resolved> {
|
) -> mlua::Result<Vec<Box<dyn Device>>> {
|
||||||
let mut devices = Vec::new();
|
let mut devices = Vec::new();
|
||||||
let mut scheduler = Scheduler::default();
|
let initial_table = match self.0 {
|
||||||
|
mlua::Value::Table(table) => table,
|
||||||
|
mlua::Value::Function(f) => f.call_async(client.clone()).await?,
|
||||||
|
_ => Err(mlua::Error::runtime(format!(
|
||||||
|
"Expected table or function, instead found: {}",
|
||||||
|
self.0.type_name()
|
||||||
|
)))?,
|
||||||
|
};
|
||||||
|
|
||||||
let mut modules: VecDeque<_> = self.0.into();
|
let mut queue: VecDeque<mlua::Table> = [initial_table].into();
|
||||||
loop {
|
loop {
|
||||||
let Some(module) = modules.pop_front() else {
|
let Some(table) = queue.pop_front() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
modules.extend(module.modules);
|
for pair in table.pairs() {
|
||||||
|
let (name, value): (String, _) = pair?;
|
||||||
|
|
||||||
if let Some(setup) = module.setup {
|
match value {
|
||||||
let result: mlua::Value = setup.call_async(client.clone()).await?;
|
mlua::Value::Table(table) => queue.push_back(table),
|
||||||
|
mlua::Value::UserData(_)
|
||||||
if result.is_nil() {
|
if let Ok(device) = Box::from_lua(value.clone(), lua) =>
|
||||||
// We ignore nil results
|
{
|
||||||
} else if let Ok(d) = <Vec<_> as FromLua>::from_lua(result.clone(), lua)
|
devices.push(device);
|
||||||
&& !d.is_empty()
|
}
|
||||||
{
|
mlua::Value::Function(f) if name == "setup" => {
|
||||||
// This is a shortcut for the common pattern of setup functions that only
|
let value: mlua::Value = f.call_async(client.clone()).await?;
|
||||||
// return devices
|
if let Some(table) = value.as_table() {
|
||||||
devices.extend(d);
|
queue.push_back(table.clone());
|
||||||
} else if let Ok(module) = FromLua::from_lua(result.clone(), lua) {
|
}
|
||||||
modules.push_back(module);
|
}
|
||||||
} else {
|
_ => {}
|
||||||
return Err(mlua::Error::runtime(
|
|
||||||
"Setup function returned data in an unexpected format",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
devices.extend(module.devices);
|
|
||||||
for (cron, f) in module.schedule {
|
|
||||||
scheduler.add_job(cron, f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Resolved { devices, scheduler })
|
Ok(devices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
impl FromLua for Setups {
|
||||||
pub struct Resolved {
|
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
|
||||||
pub devices: Vec<Box<dyn Device>>,
|
Ok(Setups(value))
|
||||||
pub scheduler: Scheduler,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Typed for Setups {
|
||||||
|
fn type_name() -> String {
|
||||||
|
"Setup".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_header() -> Option<String> {
|
||||||
|
let type_name = Self::type_name();
|
||||||
|
let client_type = WrappedAsyncClient::type_name();
|
||||||
|
|
||||||
|
Some(format!(
|
||||||
|
r#"---@alias {type_name}Function fun(mqtt_client: {client_type}): {type_name}Inner?
|
||||||
|
---@alias {type_name}Inner (DeviceInterface | {{ setup: {type_name}Function }} | {type_name}Inner)[]
|
||||||
|
---@alias {type_name} {type_name}Function | {type_name}Inner
|
||||||
|
"#,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDeviceConfig, Typed)]
|
#[derive(Debug, LuaDeviceConfig, Typed)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub fulfillment: FulfillmentConfig,
|
pub fulfillment: FulfillmentConfig,
|
||||||
#[device_config(from_lua, default)]
|
#[device_config(from_lua, default)]
|
||||||
pub modules: Modules,
|
pub modules: Option<Setups>,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub mqtt: MqttConfig,
|
pub mqtt: MqttConfig,
|
||||||
|
#[device_config(from_lua, default)]
|
||||||
|
#[typed(default)]
|
||||||
|
pub schedule: HashMap<String, ActionCallback<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FulfillmentConfig> for SocketAddr {
|
impl From<FulfillmentConfig> for SocketAddr {
|
||||||
@@ -264,25 +132,3 @@ fn default_fulfillment_ip() -> Ipv4Addr {
|
|||||||
fn default_fulfillment_port() -> u16 {
|
fn default_fulfillment_port() -> u16 {
|
||||||
7878
|
7878
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_definitions() -> String {
|
|
||||||
let mut output = "---@meta\n\n".to_string();
|
|
||||||
|
|
||||||
output +=
|
|
||||||
&FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output += &Config::generate_full().expect("Config should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output += &SetupFunction::generate_full().expect("SetupFunction should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output += &Schedule::generate_full().expect("Schedule should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output += &Module::generate_full().expect("Module should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output += &MqttConfig::generate_full().expect("MqttConfig should have a definition");
|
|
||||||
output += "\n";
|
|
||||||
output +=
|
|
||||||
&WrappedAsyncClient::generate_full().expect("WrappedAsyncClient should have a definition");
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![feature(if_let_guard)]
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
|
|||||||
+20
-28
@@ -1,37 +1,29 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use automation_lib::action_callback::ActionCallback;
|
use automation_lib::action_callback::ActionCallback;
|
||||||
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
|
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
pub async fn start_scheduler(
|
||||||
pub struct Scheduler {
|
schedule: HashMap<String, ActionCallback<()>>,
|
||||||
jobs: Vec<(String, ActionCallback<()>)>,
|
) -> Result<(), JobSchedulerError> {
|
||||||
}
|
let scheduler = JobScheduler::new().await?;
|
||||||
|
|
||||||
impl Scheduler {
|
for (s, f) in schedule {
|
||||||
pub fn add_job(&mut self, cron: String, f: ActionCallback<()>) {
|
let job = {
|
||||||
self.jobs.push((cron, f));
|
move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||||
|
let f = f.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
f.call(()).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let job = Job::new_async(s, job)?;
|
||||||
|
|
||||||
|
scheduler.add(job).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(self) -> Result<(), JobSchedulerError> {
|
scheduler.start().await
|
||||||
let scheduler = JobScheduler::new().await?;
|
|
||||||
|
|
||||||
for (s, f) in self.jobs {
|
|
||||||
let job = {
|
|
||||||
move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
|
||||||
let f = f.clone();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
|
||||||
f.call(()).await;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let job = Job::new_async(s, job)?;
|
|
||||||
|
|
||||||
scheduler.add(job).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduler.start().await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user