Compare commits

4 Commits
Author SHA1 Message Date
Dreaded_X 8babffed76 fix: No root certificates in scratch container
Build and deploy / build (push) Successful in 17m43s
Build and deploy / Deploy container (push) Successful in 34s
2026-06-20 02:42:53 +02:00
Dreaded_X b30988f869 feat: Sync printer light with room light
Build and deploy / build (push) Successful in 18m25s
Build and deploy / Deploy container (push) Successful in 37s
2026-06-20 00:44:21 +02:00
Dreaded_X aacb11aba3 feat: Add basic support for bambu printer 2026-06-20 00:44:20 +02:00
Dreaded_X 0ef3f33746 feat: Build fully static binary 2026-06-19 21:36:30 +02:00
26 changed files with 759 additions and 677 deletions
-28
View File
@@ -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 }}
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs: jobs:
build: build:
uses: infra/workflows/.gitea/workflows/docker.yaml@2bb37b698effa4fba36040d668cfa95d4abc568e uses: dreaded_x/workflows/.gitea/workflows/docker-kubernetes.yaml@ef78704b98c72e4a6b8340f9bff7b085a7bdd95c
secrets: inherit secrets: inherit
with: with:
push_manifests: false push_manifests: false
Generated
+675 -557
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -34,10 +34,10 @@ 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.2"
inventory = "0.3.24" inventory = "0.3.24"
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.6", features = [
"lua54", "lua54",
"vendored", "vendored",
"macros", "macros",
@@ -51,11 +51,11 @@ reqwest = { version = "0.13.3", features = [
"json", "json",
"rustls", "rustls",
], 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.25.1"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
serde_repr = "0.1.20" serde_repr = "0.1.20"
syn = { version = "3.0.3" } syn = { version = "2.0.117" }
thiserror = "2.0.18" thiserror = "2.0.18"
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.1"
@@ -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 -3
View File
@@ -1,4 +1,4 @@
FROM rust:1.98-alpine3.24 AS base FROM rust:1.95-alpine AS base
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
@@ -9,7 +9,7 @@ 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 RUN apk add --no-cache g++=15.2.0-r2 cmake=4.1.3-r0 make=4.4.1-r3 openssl-dev=3.5.7-r0 openssl-libs-static=3.5.7-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
@@ -21,7 +21,7 @@ ENV RELEASE_VERSION=${RELEASE_VERSION}
RUN cargo auditable build --release RUN cargo auditable build --release
FROM gcr.io/distroless/static-debian13:nonroot AS runtime FROM scratch 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 AUTOMATION__ENTRYPOINT=/app/config/config.lua
ENV LUA_PATH="/app/?.lua;;" ENV LUA_PATH="/app/?.lua;;"
+1 -1
View File
@@ -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 }
+6 -4
View File
@@ -1,6 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::InfoConfig; use automation_lib::config::InfoConfig;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::reqwest::new_client;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use google_home::device::Name; use google_home::device::Name;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
@@ -52,20 +53,21 @@ impl AirFilter {
async fn set_fan_speed(&self, speed: air_filter_types::FanSpeed) -> Result<(), Error> { async fn set_fan_speed(&self, speed: air_filter_types::FanSpeed) -> Result<(), Error> {
let message = air_filter_types::SetFanSpeed::new(speed); let message = air_filter_types::SetFanSpeed::new(speed);
let url = format!("{}/state/fan", self.config.url); let url = format!("{}/state/fan", self.config.url);
let client = reqwest::Client::new(); new_client().put(url).json(&message).send().await?;
client.put(url).json(&message).send().await?;
Ok(()) Ok(())
} }
async fn get_fan_state(&self) -> Result<air_filter_types::FanState, Error> { async fn get_fan_state(&self) -> Result<air_filter_types::FanState, Error> {
let url = format!("{}/state/fan", self.config.url); let url = format!("{}/state/fan", self.config.url);
Ok(reqwest::get(url).await?.json().await?) let client = new_client();
Ok(client.get(url).send().await?.json().await?)
} }
async fn get_sensor_data(&self) -> Result<air_filter_types::SensorData, Error> { async fn get_sensor_data(&self) -> Result<air_filter_types::SensorData, Error> {
let url = format!("{}/state/sensor", self.config.url); let url = format!("{}/state/sensor", self.config.url);
Ok(reqwest::get(url).await?.json().await?) let client = new_client();
Ok(client.get(url).send().await?.json().await?)
} }
} }
+1 -4
View File
@@ -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 -1
View File
@@ -4,6 +4,7 @@ use std::net::SocketAddr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData; use automation_lib::lua::traits::PartialUserData;
use automation_lib::reqwest::new_client;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed; use lua_typed::Typed;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
@@ -98,7 +99,7 @@ impl HueBridge {
); );
trace!(?flag, flag_id, value, "Sending request to change flag"); trace!(?flag, flag_id, value, "Sending request to change flag");
let res = reqwest::Client::new() let res = new_client()
.put(url) .put(url)
.json(&FlagMessage { flag: value }) .json(&FlagMessage { flag: value })
.send() .send()
+4 -9
View File
@@ -3,6 +3,7 @@ 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_lib::lua::traits::PartialUserData;
use automation_lib::reqwest::new_client;
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;
@@ -74,7 +75,7 @@ impl OnOff for HueGroup {
message::Action::on(false) message::Action::on(false)
}; };
let res = reqwest::Client::new() let res = new_client()
.put(self.url_set_action()) .put(self.url_set_action())
.json(&message) .json(&message)
.send() .send()
@@ -94,10 +95,7 @@ impl OnOff for HueGroup {
} }
async fn on(&self) -> Result<bool, ErrorCode> { async fn on(&self) -> Result<bool, ErrorCode> {
let res = reqwest::Client::new() let res = new_client().get(self.url_get_state()).send().await;
.get(self.url_get_state())
.send()
.await;
match res { match res {
Ok(res) => { Ok(res) => {
@@ -128,10 +126,7 @@ struct AllOn;
impl PartialUserData<HueGroup> for AllOn { impl PartialUserData<HueGroup> for AllOn {
fn add_methods<M: mlua::UserDataMethods<HueGroup>>(methods: &mut M) { fn add_methods<M: mlua::UserDataMethods<HueGroup>>(methods: &mut M) {
methods.add_async_method("all_on", async |_lua, this, ()| { methods.add_async_method("all_on", async |_lua, this, ()| {
let res = reqwest::Client::new() let res = new_client().get(this.url_get_state()).send().await;
.get(this.url_get_state())
.send()
.await;
match res { match res {
Ok(res) => { Ok(res) => {
+1 -4
View File
@@ -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) => {
+1 -4
View File
@@ -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 -4
View File
@@ -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;
} }
+2 -1
View File
@@ -4,6 +4,7 @@ use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData; use automation_lib::lua::traits::PartialUserData;
use automation_lib::reqwest::new_client;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed; use lua_typed::Typed;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
@@ -143,7 +144,7 @@ impl Ntfy {
let notification = notification.finalize(&self.config.topic); let notification = notification.finalize(&self.config.topic);
// Create the request // Create the request
let res = reqwest::Client::new() let res = new_client()
.post(self.config.url.clone()) .post(self.config.url.clone())
.json(&notification) .json(&notification)
.send() .send()
+2 -7
View File
@@ -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
+1 -4
View File
@@ -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;
} }
+1 -4
View File
@@ -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;
} }
+10 -17
View File
@@ -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}"))
+5 -10
View File
@@ -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}"))
+3 -1
View File
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
reqwest = { workspace = true }
automation_macro = { workspace = true } automation_macro = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
automation_cast = { workspace = true } automation_cast = { workspace = true }
@@ -15,9 +16,10 @@ 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 }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
webpki-root-certs = "1.0.8"
+1
View File
@@ -13,6 +13,7 @@ pub mod helpers;
pub mod lua; pub mod lua;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod reqwest;
type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::Table>; type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::Table>;
type DefinitionsFn = fn() -> String; type DefinitionsFn = fn() -> String;
+6 -5
View File
@@ -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");
+23
View File
@@ -0,0 +1,23 @@
use reqwest::{Certificate, Client};
pub fn new_client() -> Client {
println!(
"{}/{}",
std::env!("CARGO_PKG_NAME"),
std::env!("CARGO_PKG_VERSION")
);
Client::builder()
.user_agent(format!(
"{}/{}",
std::env!("CARGO_PKG_NAME"),
std::env!("CARGO_PKG_VERSION")
))
.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.map(|cert| Certificate::from_der(cert).unwrap()),
)
.build()
.expect("Client should build")
}
+1 -1
View File
@@ -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 -1
View File
@@ -13,7 +13,7 @@ function module.setup(mqtt_client)
local light = nil local light = nil
local bambu = devices.Bambu.new({ local bambu = devices.Bambu.new({
host = "10.0.0.108", host = "thalia.huizinga.lan",
device_id = secrets.printer_device_id, device_id = secrets.printer_device_id,
access_code = secrets.printer_access_code, access_code = secrets.printer_access_code,
callbacks = { callbacks = {
+2 -1
View File
@@ -1,5 +1,6 @@
use std::result; use std::result;
use automation_lib::reqwest::new_client;
use axum::extract::{FromRef, FromRequestParts}; use axum::extract::{FromRef, FromRequestParts};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::http::request::Parts; use axum::http::request::Parts;
@@ -93,7 +94,7 @@ where
// TODO: Do some discovery to find the correct url for this instead of assuming // TODO: Do some discovery to find the correct url for this instead of assuming
// TODO: I think we can also just run Authlia in front of the endpoint instead // TODO: I think we can also just run Authlia in front of the endpoint instead
// This would then give us a header containing the logged in user info? // This would then give us a header containing the logged in user info?
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url)); let mut req = new_client().get(format!("{}/userinfo", openid_url));
// Add auth header to the request if it exists // Add auth header to the request if it exists
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) { if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {