feat: Add basic support for bambu printer

This commit is contained in:
2026-06-19 04:02:58 +02:00
parent 361d799377
commit fe6b3b52e1
6 changed files with 330 additions and 1 deletions
Generated
+137
View File
@@ -42,6 +42,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -105,6 +117,7 @@ dependencies = [
"async-trait",
"automation_lib",
"automation_macro",
"bambulab",
"bytes",
"dyn-clone",
"eui48",
@@ -231,6 +244,20 @@ dependencies = [
"tracing",
]
[[package]]
name = "bambulab"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25534ead4a23a9ecee1530f1169246eabc2602ab704ffc57d41b59811167d83"
dependencies = [
"futures",
"nanoid",
"paho-mqtt",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "base64"
version = "0.22.1"
@@ -349,6 +376,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.22"
@@ -398,6 +434,21 @@ dependencies = [
"strum",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "darling"
version = "0.20.11"
@@ -576,6 +627,27 @@ dependencies = [
"serde",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -697,6 +769,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.32"
@@ -1402,6 +1480,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "nanoid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873"
dependencies = [
"rand",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1443,6 +1530,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "ordered-float"
version = "2.10.1"
@@ -1452,6 +1551,38 @@ dependencies = [
"num-traits",
]
[[package]]
name = "paho-mqtt"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23c6e899549be260b8c8d7fb6908d72eacb9a1e10229c7e294a7a8ff1c768620"
dependencies = [
"async-channel",
"crossbeam-channel",
"futures",
"futures-timer",
"libc",
"log",
"paho-mqtt-sys",
"thiserror",
]
[[package]]
name = "paho-mqtt-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e508bdadfa0d58d67792060fa447ec20729db5dc76e3dc7de9f4d29e521a43"
dependencies = [
"cmake",
"openssl-sys",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2577,6 +2708,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wakey"
version = "0.4.1"
+1 -1
View File
@@ -9,7 +9,7 @@ COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM base AS builder
RUN apk add --no-cache g++=15.2.0-r2 cmake=4.1.3-r0 make=4.4.1-r3
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!
ENV RUSTC_BOOTSTRAP=1
COPY --from=planner /app/recipe.json recipe.json
+1
View File
@@ -25,3 +25,4 @@ thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
wakey = { workspace = true }
bambulab = { version = "0.4.30", default-features = false }
+142
View File
@@ -0,0 +1,142 @@
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(())
}
}
+30
View File
@@ -1,5 +1,7 @@
#![feature(debug_closure_helpers)]
#![feature(iter_intersperse)]
mod air_filter;
mod bambu;
mod contact_sensor;
mod hue_bridge;
mod hue_group;
@@ -13,6 +15,9 @@ mod wake_on_lan;
mod washer;
mod zigbee;
use std::fmt;
use std::ops::{Deref, DerefMut};
use automation_lib::Module;
use automation_lib::device::{Device, LuaDeviceCreate};
use tracing::{debug, warn};
@@ -20,6 +25,31 @@ use tracing::{debug, warn};
type DeviceNameFn = fn() -> String;
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 {
name_fn: DeviceNameFn,
register_fn: RegisterDeviceFn,
+19
View File
@@ -24,6 +24,25 @@ function devices.AirFilter.new(config) end
---@field url string
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
---@field name string
---@field room (string)?