Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8ec0178c34
|
|||
|
300ad171ef
|
|||
|
9979ab3446
|
|||
|
b66357749b
|
|||
|
5a2c1b0a13
|
|||
|
ccd9460e76
|
|||
|
00c32e8993
|
|||
|
8c6adae3ae
|
|||
|
2158bde1c2
|
|||
|
b547f66d86
|
|||
|
f3de8e36ea
|
|||
|
44f2c57819
|
@@ -9,10 +9,10 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: dreaded_x/workflows/.gitea/workflows/rust-kubernetes.yaml@22ee0c1788a8d2157db87d6a6f8dbe520fe48592
|
||||
uses: dreaded_x/workflows/.gitea/workflows/docker-kubernetes.yaml@ef78704b98c72e4a6b8340f9bff7b085a7bdd95c
|
||||
secrets: inherit
|
||||
with:
|
||||
upload_manifests: false
|
||||
push_manifests: false
|
||||
|
||||
deploy:
|
||||
name: Deploy container
|
||||
@@ -26,6 +26,10 @@ jobs:
|
||||
docker stop 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
|
||||
run: |
|
||||
docker create \
|
||||
@@ -37,7 +41,7 @@ jobs:
|
||||
-e AUTOMATION__SECRETS__MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \
|
||||
-e AUTOMATION__SECRETS__HUE_TOKEN=${{ secrets.HUE_TOKEN }} \
|
||||
-e AUTOMATION__SECRETS__NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \
|
||||
git.huizinga.dev/dreaded_x/automation_rs@${{ needs.build.outputs.digest }}
|
||||
$(echo ${{ toJSON(needs.build.outputs.images) }} | jq .automation -r)
|
||||
|
||||
docker network connect web automation_rs
|
||||
|
||||
|
||||
Generated
+1045
-575
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -16,28 +16,28 @@ members = [
|
||||
|
||||
[workspace.dependencies]
|
||||
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.102"
|
||||
async-trait = "0.1.89"
|
||||
automation_cast = { path = "./automation_cast" }
|
||||
automation_devices = { path = "./automation_devices" }
|
||||
automation_lib = { path = "./automation_lib" }
|
||||
automation_macro = { path = "./automation_macro" }
|
||||
axum = "0.8.4"
|
||||
bytes = "1.10.1"
|
||||
axum = "0.8.9"
|
||||
bytes = "1.11.1"
|
||||
dyn-clone = "1.0.20"
|
||||
eui48 = { version = "1.1.0", features = [
|
||||
"disp_hexstring",
|
||||
"serde",
|
||||
], default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.32"
|
||||
google_home = { path = "./google_home/google_home" }
|
||||
google_home_macro = { path = "./google_home/google_home_macro" }
|
||||
hostname = "0.4.1"
|
||||
inventory = "0.3.21"
|
||||
hostname = "0.4.2"
|
||||
inventory = "0.3.24"
|
||||
itertools = "0.14.0"
|
||||
json_value_merge = "2.0.1"
|
||||
lua_typed = { git = "https://git.huizinga.dev/Dreaded_X/lua_typed" }
|
||||
mlua = { version = "0.11.3", features = [
|
||||
mlua = { version = "0.11.6", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"macros",
|
||||
@@ -45,23 +45,23 @@ mlua = { version = "0.11.3", features = [
|
||||
"async",
|
||||
"send",
|
||||
] }
|
||||
proc-macro2 = "1.0.101"
|
||||
quote = "1.0.40"
|
||||
reqwest = { version = "0.12.23", features = [
|
||||
proc-macro2 = "1.0.106"
|
||||
quote = "1.0.45"
|
||||
reqwest = { version = "0.13.3", features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"rustls",
|
||||
], default-features = false } # Use rustls, since the other packages also use rustls
|
||||
rumqttc = "0.24.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
rumqttc = "0.25.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
serde_repr = "0.1.20"
|
||||
syn = { version = "2.0.106" }
|
||||
thiserror = "2.0.16"
|
||||
syn = { version = "2.0.117" }
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
tokio-cron-scheduler = "0.15.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.20"
|
||||
wakey = "0.3.0"
|
||||
tokio-cron-scheduler = "0.15.1"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
wakey = "0.4.1"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -70,7 +70,7 @@ automation_devices = { workspace = true }
|
||||
automation_lib = { workspace = true }
|
||||
automation_macro = { path = "./automation_macro" }
|
||||
axum = { workspace = true }
|
||||
config = { version = "0.15.15", default-features = false, features = [
|
||||
config = { version = "0.15.22", default-features = false, features = [
|
||||
"async",
|
||||
"toml",
|
||||
] }
|
||||
|
||||
+5
-2
@@ -1,8 +1,9 @@
|
||||
FROM rust:1.89 AS base
|
||||
FROM rust:1.95 AS base
|
||||
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
RUN cargo install cargo-chef --locked --version 0.1.71 && \
|
||||
cargo install cargo-auditable --locked --version 0.6.6
|
||||
WORKDIR /app
|
||||
RUN rustup toolchain install
|
||||
|
||||
FROM base AS planner
|
||||
COPY . .
|
||||
@@ -19,7 +20,9 @@ ARG RELEASE_VERSION
|
||||
ENV RELEASE_VERSION=${RELEASE_VERSION}
|
||||
RUN cargo auditable build --release
|
||||
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime
|
||||
FROM gcr.io/distroless/cc-debian13:nonroot AS runtime
|
||||
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
|
||||
CMD [ "/app/automation" ]
|
||||
|
||||
@@ -4,9 +4,9 @@ Custom home automation solution with Google Home integration and lua scripting.
|
||||
|
||||
## Development
|
||||
|
||||
This repository uses [pre-commit](https://pre-commit.com) to make sure everything is ready to go when committing.
|
||||
This repository uses [prek](https://prek.j178.dev/) to make sure everything is ready to go when committing.
|
||||
Install the pre-commit hooks by running the following command:
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
prek install
|
||||
```
|
||||
|
||||
@@ -25,3 +25,4 @@ thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
wakey = { workspace = true }
|
||||
bambulab = { version = "0.4.30", default-features = false }
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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::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 => {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_lib::lua::traits::PartialUserData;
|
||||
use automation_macro::{Device, LuaDeviceConfig};
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::OnOff;
|
||||
@@ -25,6 +26,7 @@ crate::register_type!(Config);
|
||||
|
||||
#[derive(Debug, Clone, Device)]
|
||||
#[device(traits(OnOff))]
|
||||
#[device(extra_user_data = AllOn)]
|
||||
pub struct HueGroup {
|
||||
config: Config,
|
||||
}
|
||||
@@ -122,6 +124,47 @@ 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 {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -164,5 +207,9 @@ mod message {
|
||||
pub fn any_on(&self) -> bool {
|
||||
self.state.any_on
|
||||
}
|
||||
|
||||
pub fn all_on(&self) -> bool {
|
||||
self.state.all_on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,15 +122,11 @@ impl OnMqtt for HueSwitch {
|
||||
Action::LeftHold => self.config.left_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
|
||||
Action::RightHoldRelease => {
|
||||
if self.config.right_hold_callback.is_empty() {
|
||||
self.config.right_callback.call(self.clone()).await
|
||||
}
|
||||
Action::RightHoldRelease 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() {
|
||||
self.config.left_callback.call(self.clone()).await
|
||||
}
|
||||
Action::LeftHoldRelease if self.config.left_hold_callback.is_empty() => {
|
||||
self.config.left_callback.call(self.clone()).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![feature(iter_intersperse)]
|
||||
#![feature(iterator_try_collect)]
|
||||
mod device;
|
||||
mod lua_device_config;
|
||||
|
||||
@@ -25,6 +25,13 @@ function module.setup(mqtt_client)
|
||||
group_id = 3,
|
||||
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({
|
||||
name = "Air Filter",
|
||||
@@ -32,13 +39,36 @@ function module.setup(mqtt_client)
|
||||
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({
|
||||
name = "Switch",
|
||||
room = "Bedroom",
|
||||
client = mqtt_client,
|
||||
topic = helper.mqtt_z2m("bedroom/switch"),
|
||||
left_callback = function()
|
||||
lights:set_on(not lights:on())
|
||||
local on = not lights:all_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,
|
||||
left_hold_callback = function()
|
||||
lights_relax:set_on(true)
|
||||
@@ -61,6 +91,7 @@ function module.setup(mqtt_client)
|
||||
lights,
|
||||
lights_relax,
|
||||
air_filter,
|
||||
wardrobe_door,
|
||||
switch,
|
||||
window,
|
||||
},
|
||||
|
||||
@@ -4,15 +4,35 @@ local helper = require("config.helper")
|
||||
local presence = require("config.presence")
|
||||
local windows = require("config.windows")
|
||||
|
||||
local secrets = require("automation:secrets")
|
||||
|
||||
--- @type Module
|
||||
local module = {}
|
||||
|
||||
function module.setup(mqtt_client)
|
||||
local light = devices.LightOnOff.new({
|
||||
local light = nil
|
||||
|
||||
local bambu = devices.Bambu.new({
|
||||
host = "thalia.huizinga.lan",
|
||||
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",
|
||||
room = "Guest Room",
|
||||
topic = helper.mqtt_z2m("guest/light"),
|
||||
client = mqtt_client,
|
||||
callback = function(_, state)
|
||||
bambu:set_on(state.state)
|
||||
end,
|
||||
})
|
||||
presence.turn_off_when_away(light)
|
||||
|
||||
@@ -25,10 +45,19 @@ function module.setup(mqtt_client)
|
||||
})
|
||||
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 {
|
||||
light,
|
||||
window,
|
||||
printer,
|
||||
bambu,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -59,26 +59,21 @@ function module.setup(mqtt_client)
|
||||
})
|
||||
hallway_automation.set_trash(trash)
|
||||
|
||||
---@param duration number
|
||||
---@return fun(_, open: boolean)
|
||||
local function frontdoor_presence(duration)
|
||||
local timeout = utils.Timeout.new()
|
||||
local timeout = utils.Timeout.new()
|
||||
local function frontdoor_presence(_, open)
|
||||
if open then
|
||||
timeout:cancel()
|
||||
|
||||
return function(_, open)
|
||||
if open then
|
||||
timeout:cancel()
|
||||
|
||||
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)
|
||||
if not presence.overall_presence() then
|
||||
mqtt_client:send_message(helper.mqtt_automation("presence/contact/frontdoor"), {
|
||||
state = true,
|
||||
updated = utils.get_epoch(),
|
||||
})
|
||||
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
|
||||
|
||||
@@ -89,7 +84,7 @@ function module.setup(mqtt_client)
|
||||
topic = helper.mqtt_z2m("hallway/frontdoor"),
|
||||
client = mqtt_client,
|
||||
callback = {
|
||||
frontdoor_presence(debug.debug_mode and 10 or 15 * 60),
|
||||
frontdoor_presence,
|
||||
hallway_automation.door_callback,
|
||||
},
|
||||
battery_callback = battery.callback,
|
||||
|
||||
@@ -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)?
|
||||
@@ -116,6 +135,9 @@ devices.HueGroup = {}
|
||||
---@param config HueGroupConfig
|
||||
---@return HueGroup
|
||||
function devices.HueGroup.new(config) end
|
||||
---@async
|
||||
---@return boolean
|
||||
function HueGroup:all_on() end
|
||||
|
||||
---@class HueGroupConfig
|
||||
---@field identifier string
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
variable "TAG_BASE" {}
|
||||
variable "RELEASE_VERSION" {}
|
||||
|
||||
group "default" {
|
||||
targets = ["automation"]
|
||||
}
|
||||
|
||||
target "docker-metadata-action" {}
|
||||
|
||||
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]
|
||||
channel = "nightly-2025-08-20"
|
||||
channel = "nightly-2026-05-12"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
profile = "minimal"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![feature(if_let_guard)]
|
||||
|
||||
pub mod config;
|
||||
pub mod schedule;
|
||||
pub mod secret;
|
||||
|
||||
Reference in New Issue
Block a user