Compare commits

...

8 Commits

Author SHA1 Message Date
34175a20bd chore: Set RUST_LOG to something sensible by default when running with cargo
All checks were successful
Build and deploy / build (push) Successful in 10m21s
Build and deploy / Deploy container (push) Successful in 37s
2025-09-05 03:34:51 +02:00
c2ba4ffbb5 feat(config)!: Reworked how configuration is loaded
All checks were successful
Build and deploy / build (push) Successful in 11m1s
Build and deploy / Deploy container (push) Successful in 40s
The environment variable `AUTOMATION_CONFIG` has been renamed to
`AUTOMATION__ENTRYPOINT` and can now also be set in `automation.toml` by
specifying:
```
automation = "<path>"
```

Directly accessing the environment variables in lua in no longer
possible. To pass in configuration or secrets you can now instead make
use of the `variables` and `secrets` modules.

To set values in these modules you can either specify them in
`automation.toml`:
```
[variables]
<name> = <value>

[secrets]
<name> = <value>
```
Note that these values will get converted to a string.

You can also specify the environment variables
`AUTOMATION__VARIABLES__<name>` and `AUTOMATION__SECRETS__<name>` to
set variables and secrets respectively. By adding the suffix `__FILE` to
the environment variable name the contents of a file can be loaded into
the variable or secret.

Note that variables and secrets are identical in functionality and the
name difference exists purely to make it clear that secret values are
meant to be kept secret.
2025-09-05 03:14:44 +02:00
7a9f464e61 feat(config)!: Move new_mqtt_client out of global automation table into separate module
All checks were successful
Build and deploy / build (push) Successful in 10m30s
Build and deploy / Deploy container (push) Successful in 45s
The function `new_mqtt_client` was the last remaining entry in the
global `automation` table. The function was renamed to `new` and placed
in the new `mqtt` module. As `automation` is now empty, it has been
removed.
2025-09-04 04:28:03 +02:00
3d5f6c308c feat(config)!: Move device_manager out of global automation table into separate module
Moved `automation.device_manager` into a separate module called
`device_manager`
2025-09-04 04:28:03 +02:00
c6a6265d6c feat(config)!: Move util out of global automation table into separate module
Move `automation.util` into a separate module called `utils`.
2025-09-04 04:28:02 +02:00
d6816bc693 feat(config)!: Fulfillment config is now returned at the end of the config
Previously the fulfillment config was set by setting
`automation.fulfillment`, this will no longer work in the future when
the global automation gets split into modules.
2025-09-04 04:28:02 +02:00
c8d5df753f style: Sort crates by name 2025-09-04 04:28:02 +02:00
96cb814495 style: Enforce conventional commits formatting 2025-09-04 04:28:02 +02:00
15 changed files with 363 additions and 216 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[env]
RUST_LOG = "automation=debug"

View File

@@ -34,9 +34,9 @@ jobs:
--name automation_rs \
--network mqtt \
-e RUST_LOG=automation=debug \
-e MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \
-e HUE_TOKEN=${{ secrets.HUE_TOKEN }} \
-e NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \
-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 }}
docker network connect web automation_rs

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
.env
automation.toml

View File

@@ -1,3 +1,9 @@
default_install_hook_types:
- pre-commit
- commit-msg
default_stages:
- pre-commit
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
@@ -11,6 +17,13 @@ repos:
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.2.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: []
- repo: https://github.com/JohnnyMorganz/StyLua
rev: v2.1.0
hooks:

75
Cargo.lock generated
View File

@@ -95,6 +95,7 @@ dependencies = [
"automation_devices",
"automation_lib",
"axum",
"config",
"dotenvy",
"google_home",
"hostname",
@@ -327,6 +328,19 @@ dependencies = [
"windows-link",
]
[[package]]
name = "config"
version = "0.15.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba"
dependencies = [
"async-trait",
"pathdiff",
"serde",
"toml",
"winnow",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -466,7 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1262,6 +1276,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1409,7 +1429,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1586,7 +1606,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1784,6 +1804,15 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -2035,6 +2064,37 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.5.2"
@@ -2491,6 +2551,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]]
name = "winsafe"
version = "0.0.19"

View File

@@ -5,34 +5,22 @@ edition = "2024"
[workspace]
members = [
"automation_macro",
"automation_cast",
"google_home/google_home",
"google_home/google_home_macro",
"automation_devices",
"automation_lib",
"automation_macro",
"google_home/google_home",
"google_home/google_home_macro",
]
[workspace.dependencies]
mlua = { version = "0.11.3", features = [
"lua54",
"vendored",
"macros",
"serialize",
"async",
"send",
] }
automation_macro = { path = "./automation_macro" }
automation_cast = { path = "./automation_cast" }
automation_lib = { path = "./automation_lib" }
automation_devices = { path = "./automation_devices" }
google_home = { path = "./google_home/google_home" }
google_home_macro = { path = "./google_home/google_home_macro" }
tokio = { version = "1", features = ["rt-multi-thread"] }
rumqttc = "0.24.0"
tracing = "0.1.41"
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
anyhow = "1.0.99"
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"
dotenvy = "0.15.7"
@@ -42,45 +30,61 @@ eui48 = { version = "1.1.0", features = [
"serde",
], default-features = false }
futures = "0.3.31"
google_home = { path = "./google_home/google_home" }
google_home_macro = { path = "./google_home/google_home_macro" }
hostname = "0.4.1"
indexmap = { version = "2.11.0", features = ["serde"] }
itertools = "0.14.0"
json_value_merge = "2.0.1"
mlua = { version = "0.11.3", features = [
"lua54",
"vendored",
"macros",
"serialize",
"async",
"send",
] }
proc-macro2 = "1.0.101"
quote = "1.0.40"
reqwest = { version = "0.12.23", features = [
"json",
"rustls-tls",
], 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"
serde_repr = "0.1.20"
syn = { version = "2.0.106", features = ["extra-traits", "full"] }
thiserror = "2.0.16"
tokio = { version = "1", features = ["rt-multi-thread"] }
tokio-cron-scheduler = "0.14.0"
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
uuid = "1.18.1"
wakey = "0.3.0"
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
[dependencies]
async-trait = { workspace = true }
automation_lib = { workspace = true }
automation_devices = { workspace = true }
google_home = { workspace = true }
mlua = { workspace = true }
tokio = { workspace = true }
hostname = { workspace = true }
rumqttc = { workspace = true }
axum = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
automation_devices = { workspace = true }
automation_lib = { workspace = true }
axum = { workspace = true }
config = { version = "0.15.15", default-features = false, features = [
"async",
"toml",
] }
dotenvy = { workspace = true }
tracing-subscriber = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
serde_json = { workspace = true }
google_home = { workspace = true }
hostname = { workspace = true }
mlua = { workspace = true }
reqwest = { workspace = true }
rumqttc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[patch.crates-io]
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }

View File

@@ -21,6 +21,6 @@ RUN cargo auditable build --release
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime
COPY --from=builder /app/target/release/automation /app/automation
ENV AUTOMATION_CONFIG=/app/config.lua
ENV AUTOMATION__ENTRYPOINT=/app/config.lua
COPY ./config.lua /app/config.lua
CMD [ "/app/automation" ]

View File

@@ -3,6 +3,4 @@ name = "automation_cast"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -4,22 +4,22 @@ version = "0.1.0"
edition = "2024"
[dependencies]
air_filter_types = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
automation_lib = { workspace = true }
automation_macro = { workspace = true }
bytes = { workspace = true }
dyn-clone = { workspace = true }
eui48 = { workspace = true }
google_home = { workspace = true }
mlua = { workspace = true }
async-trait = { workspace = true }
dyn-clone = { workspace = true }
reqwest = { workspace = true }
rumqttc = { workspace = true }
tokio = { workspace = true }
serde_repr = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
reqwest = { workspace = true } # Use rustls, since the other packages also use rustls
anyhow = { workspace = true }
bytes = { workspace = true }
serde_json = { workspace = true }
serde_repr = { workspace = true }
thiserror = { workspace = true }
eui48 = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
wakey = { workspace = true }
air_filter_types = { workspace = true }

View File

@@ -4,19 +4,19 @@ version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = { workspace = true }
automation_cast = { workspace = true }
bytes = { workspace = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
google_home = { workspace = true }
indexmap = { workspace = true }
mlua = { workspace = true }
rumqttc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
bytes = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
indexmap = { workspace = true }
tokio = { workspace = true }
tokio-cron-scheduler = { workspace = true }
mlua = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
dyn-clone = { workspace = true }

View File

@@ -1,13 +1,15 @@
local device_manager = require("device_manager")
local utils = require("utils")
local secrets = require("secrets")
local debug = require("variables").debug or false
print(debug)
print(_VERSION)
local host = automation.util.get_hostname()
local host = utils.get_hostname()
print("Running @" .. host)
local debug, value = pcall(automation.util.get_env, "DEBUG")
if debug and value ~= "true" then
debug = false
end
local function mqtt_z2m(topic)
return "zigbee2mqtt/" .. topic
end
@@ -16,23 +18,23 @@ local function mqtt_automation(topic)
return "automation/" .. topic
end
automation.fulfillment = {
local fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc",
}
local mqtt_client = automation.new_mqtt_client({
local mqtt_client = require("mqtt").new({
host = ((host == "zeus" or host == "hephaestus") and "olympus.lan.huizinga.dev") or "mosquitto",
port = 8883,
client_name = "automation-" .. host,
username = "mqtt",
password = automation.util.get_env("MQTT_PASSWORD"),
password = secrets.mqtt_password,
tls = host == "zeus" or host == "hephaestus",
})
local ntfy = Ntfy.new({
topic = automation.util.get_env("NTFY_TOPIC"),
topic = secrets.ntfy_topic,
})
automation.device_manager:add(ntfy)
device_manager:add(ntfy)
local low_battery = {}
local function check_battery(device, battery)
@@ -44,7 +46,7 @@ local function check_battery(device, battery)
low_battery[id] = nil
end
end
automation.device_manager:schedule("0 0 21 */1 * *", function()
device_manager:schedule("0 0 21 */1 * *", function()
-- Don't send notifications if there are now devices with low battery
if next(low_battery) == nil then
print("No devices with low battery")
@@ -82,7 +84,7 @@ local presence_system = Presence.new({
end
end,
})
automation.device_manager:add(presence_system)
device_manager:add(presence_system)
on_presence:add(function(presence)
ntfy:send_notification({
title = "Presence",
@@ -105,7 +107,7 @@ end)
on_presence:add(function(presence)
mqtt_client:send_message(mqtt_automation("debug") .. "/presence", {
state = presence,
updated = automation.util.get_epoch(),
updated = utils.get_epoch(),
})
end)
@@ -122,7 +124,7 @@ local on_light = {
self[#self + 1] = f
end,
}
automation.device_manager:add(LightSensor.new({
device_manager:add(LightSensor.new({
identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"),
client = mqtt_client,
@@ -139,12 +141,12 @@ automation.device_manager:add(LightSensor.new({
on_light:add(function(light)
mqtt_client:send_message(mqtt_automation("debug") .. "/darkness", {
state = not light,
updated = automation.util.get_epoch(),
updated = utils.get_epoch(),
})
end)
local hue_ip = "10.0.0.102"
local hue_token = automation.util.get_env("HUE_TOKEN")
local hue_token = secrets.hue_token
local hue_bridge = HueBridge.new({
identifier = "hue_bridge",
@@ -155,7 +157,7 @@ local hue_bridge = HueBridge.new({
darkness = 43,
},
})
automation.device_manager:add(hue_bridge)
device_manager:add(hue_bridge)
on_light:add(function(light)
hue_bridge:set_flag("darkness", not light)
end)
@@ -170,7 +172,7 @@ local kitchen_lights = HueGroup.new({
group_id = 7,
scene_id = "7MJLG27RzeRAEVJ",
})
automation.device_manager:add(kitchen_lights)
device_manager:add(kitchen_lights)
local living_lights = HueGroup.new({
identifier = "living_lights",
ip = hue_ip,
@@ -178,7 +180,7 @@ local living_lights = HueGroup.new({
group_id = 1,
scene_id = "SNZw7jUhQ3cXSjkj",
})
automation.device_manager:add(living_lights)
device_manager:add(living_lights)
local living_lights_relax = HueGroup.new({
identifier = "living_lights",
ip = hue_ip,
@@ -186,9 +188,9 @@ local living_lights_relax = HueGroup.new({
group_id = 1,
scene_id = "eRJ3fvGHCcb6yNw",
})
automation.device_manager:add(living_lights_relax)
device_manager:add(living_lights_relax)
automation.device_manager:add(HueSwitch.new({
device_manager:add(HueSwitch.new({
name = "Switch",
room = "Living",
client = mqtt_client,
@@ -205,7 +207,7 @@ automation.device_manager:add(HueSwitch.new({
battery_callback = check_battery,
}))
automation.device_manager:add(WakeOnLAN.new({
device_manager:add(WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"),
@@ -221,7 +223,7 @@ local living_mixer = OutletOnOff.new({
client = mqtt_client,
})
turn_off_when_away(living_mixer)
automation.device_manager:add(living_mixer)
device_manager:add(living_mixer)
local living_speakers = OutletOnOff.new({
name = "Speakers",
room = "Living Room",
@@ -229,9 +231,9 @@ local living_speakers = OutletOnOff.new({
client = mqtt_client,
})
turn_off_when_away(living_speakers)
automation.device_manager:add(living_speakers)
device_manager:add(living_speakers)
automation.device_manager:add(IkeaRemote.new({
device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Living Room",
client = mqtt_client,
@@ -280,13 +282,13 @@ local kettle = OutletPower.new({
callback = kettle_timeout(),
})
turn_off_when_away(kettle)
automation.device_manager:add(kettle)
device_manager:add(kettle)
local function set_kettle(_, on)
kettle:set_on(on)
end
automation.device_manager:add(IkeaRemote.new({
device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Bedroom",
client = mqtt_client,
@@ -296,7 +298,7 @@ automation.device_manager:add(IkeaRemote.new({
battery_callback = check_battery,
}))
automation.device_manager:add(IkeaRemote.new({
device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Kitchen",
client = mqtt_client,
@@ -327,9 +329,9 @@ local bathroom_light = LightOnOff.new({
client = mqtt_client,
callback = off_timeout(debug and 60 or 45 * 60),
})
automation.device_manager:add(bathroom_light)
device_manager:add(bathroom_light)
automation.device_manager:add(Washer.new({
device_manager:add(Washer.new({
identifier = "bathroom_washer",
topic = mqtt_z2m("bathroom/washer"),
client = mqtt_client,
@@ -344,7 +346,7 @@ automation.device_manager:add(Washer.new({
end,
}))
automation.device_manager:add(OutletOnOff.new({
device_manager:add(OutletOnOff.new({
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
@@ -359,7 +361,7 @@ local workbench_outlet = OutletOnOff.new({
client = mqtt_client,
})
turn_off_when_away(workbench_outlet)
automation.device_manager:add(workbench_outlet)
device_manager:add(workbench_outlet)
local workbench_light = LightColorTemperature.new({
name = "Light",
@@ -368,10 +370,10 @@ local workbench_light = LightColorTemperature.new({
client = mqtt_client,
})
turn_off_when_away(workbench_light)
automation.device_manager:add(workbench_light)
device_manager:add(workbench_light)
local delay_color_temp = Timeout.new()
automation.device_manager:add(IkeaRemote.new({
device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Workbench",
client = mqtt_client,
@@ -401,7 +403,7 @@ local hallway_top_light = HueGroup.new({
group_id = 83,
scene_id = "QeufkFDICEHWeKJ7",
})
automation.device_manager:add(HueSwitch.new({
device_manager:add(HueSwitch.new({
name = "SwitchBottom",
room = "Hallway",
client = mqtt_client,
@@ -411,7 +413,7 @@ automation.device_manager:add(HueSwitch.new({
end,
battery_callback = check_battery,
}))
automation.device_manager:add(HueSwitch.new({
device_manager:add(HueSwitch.new({
name = "SwitchTop",
room = "Hallway",
client = mqtt_client,
@@ -482,7 +484,7 @@ local hallway_storage = LightBrightness.new({
end,
})
turn_off_when_away(hallway_storage)
automation.device_manager:add(hallway_storage)
device_manager:add(hallway_storage)
local hallway_bottom_lights = HueGroup.new({
identifier = "hallway_bottom_lights",
@@ -491,7 +493,7 @@ local hallway_bottom_lights = HueGroup.new({
group_id = 81,
scene_id = "3qWKxGVadXFFG4o",
})
automation.device_manager:add(hallway_bottom_lights)
device_manager:add(hallway_bottom_lights)
hallway_light_automation.group = {
set_on = function(on)
@@ -515,7 +517,7 @@ setmetatable(frontdoor_presence, {
if not presence_system:overall_presence() then
mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), {
state = true,
updated = automation.util.get_epoch(),
updated = utils.get_epoch(),
})
end
else
@@ -526,7 +528,7 @@ setmetatable(frontdoor_presence, {
end,
})
automation.device_manager:add(IkeaRemote.new({
device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Hallway",
client = mqtt_client,
@@ -552,7 +554,7 @@ local hallway_frontdoor = ContactSensor.new({
end,
battery_callback = check_battery,
})
automation.device_manager:add(hallway_frontdoor)
device_manager:add(hallway_frontdoor)
hallway_light_automation.door = hallway_frontdoor
local hallway_trash = ContactSensor.new({
@@ -566,7 +568,7 @@ local hallway_trash = ContactSensor.new({
end,
battery_callback = check_battery,
})
automation.device_manager:add(hallway_trash)
device_manager:add(hallway_trash)
hallway_light_automation.trash = hallway_trash
local guest_light = LightOnOff.new({
@@ -576,14 +578,14 @@ local guest_light = LightOnOff.new({
client = mqtt_client,
})
turn_off_when_away(guest_light)
automation.device_manager:add(guest_light)
device_manager:add(guest_light)
local bedroom_air_filter = AirFilter.new({
name = "Air Filter",
room = "Bedroom",
url = "http://10.0.0.103",
})
automation.device_manager:add(bedroom_air_filter)
device_manager:add(bedroom_air_filter)
local bedroom_lights = HueGroup.new({
identifier = "bedroom_lights",
@@ -592,7 +594,7 @@ local bedroom_lights = HueGroup.new({
group_id = 3,
scene_id = "PvRs-lGD4VRytL9",
})
automation.device_manager:add(bedroom_lights)
device_manager:add(bedroom_lights)
local bedroom_lights_relax = HueGroup.new({
identifier = "bedroom_lights",
ip = hue_ip,
@@ -600,9 +602,9 @@ local bedroom_lights_relax = HueGroup.new({
group_id = 3,
scene_id = "60tfTyR168v2csz",
})
automation.device_manager:add(bedroom_lights_relax)
device_manager:add(bedroom_lights_relax)
automation.device_manager:add(HueSwitch.new({
device_manager:add(HueSwitch.new({
name = "Switch",
room = "Bedroom",
client = mqtt_client,
@@ -616,7 +618,7 @@ automation.device_manager:add(HueSwitch.new({
battery_callback = check_battery,
}))
automation.device_manager:add(ContactSensor.new({
device_manager:add(ContactSensor.new({
name = "Balcony",
room = "Living Room",
sensor_type = "Door",
@@ -624,21 +626,21 @@ automation.device_manager:add(ContactSensor.new({
client = mqtt_client,
battery_callback = check_battery,
}))
automation.device_manager:add(ContactSensor.new({
device_manager:add(ContactSensor.new({
name = "Window",
room = "Living Room",
topic = mqtt_z2m("living/window"),
client = mqtt_client,
battery_callback = check_battery,
}))
automation.device_manager:add(ContactSensor.new({
device_manager:add(ContactSensor.new({
name = "Window",
room = "Bedroom",
topic = mqtt_z2m("bedroom/window"),
client = mqtt_client,
battery_callback = check_battery,
}))
automation.device_manager:add(ContactSensor.new({
device_manager:add(ContactSensor.new({
name = "Window",
room = "Guest Room",
topic = mqtt_z2m("guest/window"),
@@ -653,9 +655,9 @@ local storage_light = LightBrightness.new({
client = mqtt_client,
})
turn_off_when_away(storage_light)
automation.device_manager:add(storage_light)
device_manager:add(storage_light)
automation.device_manager:add(ContactSensor.new({
device_manager:add(ContactSensor.new({
name = "Door",
room = "Storage",
sensor_type = "Door",
@@ -671,9 +673,11 @@ automation.device_manager:add(ContactSensor.new({
battery_callback = check_battery,
}))
automation.device_manager:schedule("0 0 19 * * *", function()
device_manager:schedule("0 0 19 * * *", function()
bedroom_air_filter:set_on(true)
end)
automation.device_manager:schedule("0 0 20 * * *", function()
device_manager:schedule("0 0 20 * * *", function()
bedroom_air_filter:set_on(false)
end)
return fulfillment

View File

@@ -3,15 +3,13 @@ name = "google_home"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
automation_cast = { workspace = true }
futures = { workspace = true }
google_home_macro = { workspace = true }
json_value_merge = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
json_value_merge = { workspace = true }

17
src/config.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_entrypoint")]
pub entrypoint: String,
#[serde(default)]
pub variables: HashMap<String, String>,
#[serde(default)]
pub secrets: HashMap<String, String>,
}
fn default_entrypoint() -> String {
"./config.lua".into()
}

View File

@@ -1,4 +1,6 @@
#![feature(iter_intersperse)]
mod config;
mod secret;
mod web;
use std::net::SocketAddr;
@@ -6,7 +8,7 @@ use std::path::Path;
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::anyhow;
use ::config::{Environment, File};
use automation_lib::config::{FulfillmentConfig, MqttConfig};
use automation_lib::device_manager::DeviceManager;
use automation_lib::helpers;
@@ -15,6 +17,7 @@ use axum::extract::{FromRef, State};
use axum::http::StatusCode;
use axum::routing::post;
use axum::{Json, Router};
use config::Config;
use dotenvy::dotenv;
use google_home::{GoogleHome, Request, Response};
use mlua::LuaSerdeExt;
@@ -23,6 +26,8 @@ use tokio::net::TcpListener;
use tracing::{debug, error, info, warn};
use web::{ApiError, User};
use crate::secret::EnvironmentSecretFile;
#[derive(Clone)]
struct AppState {
pub openid_url: String,
@@ -70,14 +75,27 @@ async fn app() -> anyhow::Result<()> {
dotenv().ok();
tracing_subscriber::fmt::init();
// console_subscriber::init();
let config: Config = ::config::Config::builder()
.add_source(
File::with_name(&format!("{}.toml", std::env!("CARGO_PKG_NAME"))).required(false),
)
.add_source(
Environment::default()
.prefix(std::env!("CARGO_PKG_NAME"))
.separator("__"),
)
.add_source(EnvironmentSecretFile::default())
.build()
.unwrap()
.try_deserialize()
.unwrap();
info!("Starting automation_rs...");
// Setup the device handler
let device_manager = DeviceManager::new().await;
let fulfillment_config = {
let lua = mlua::Lua::new();
lua.set_warning_function(|_lua, text, _cont| {
@@ -118,9 +136,9 @@ async fn app() -> anyhow::Result<()> {
})?;
lua.globals().set("print", print)?;
let automation = lua.create_table()?;
let mqtt = lua.create_table()?;
let event_channel = device_manager.event_channel();
let new_mqtt_client = lua.create_function(move |lua, config: mlua::Value| {
let mqtt_new = lua.create_function(move |lua, config: mlua::Value| {
let config: MqttConfig = lua.from_value(config)?;
// Create a mqtt client
@@ -130,56 +148,36 @@ async fn app() -> anyhow::Result<()> {
Ok(WrappedAsyncClient(client))
})?;
mqtt.set("new", mqtt_new)?;
lua.register_module("mqtt", mqtt)?;
automation.set("new_mqtt_client", new_mqtt_client)?;
automation.set("device_manager", device_manager.clone())?;
lua.register_module("device_manager", device_manager.clone())?;
let util = lua.create_table()?;
let get_env = lua.create_function(|_lua, name: String| {
std::env::var(name).map_err(mlua::ExternalError::into_lua_err)
})?;
util.set("get_env", get_env)?;
lua.register_module("variables", lua.to_value(&config.variables)?)?;
lua.register_module("secrets", lua.to_value(&config.secrets)?)?;
let utils = lua.create_table()?;
let get_hostname = lua.create_function(|_lua, ()| {
hostname::get()
.map(|name| name.to_str().unwrap_or("unknown").to_owned())
.map_err(mlua::ExternalError::into_lua_err)
})?;
util.set("get_hostname", get_hostname)?;
utils.set("get_hostname", get_hostname)?;
let get_epoch = lua.create_function(|_lua, ()| {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time is after UNIX EPOCH")
.as_millis())
})?;
util.set("get_epoch", get_epoch)?;
automation.set("util", util)?;
lua.globals().set("automation", automation)?;
utils.set("get_epoch", get_epoch)?;
lua.register_module("utils", utils)?;
automation_devices::register_with_lua(&lua)?;
helpers::register_with_lua(&lua)?;
// TODO: Make this not hardcoded
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());
let config_path = Path::new(&config_filename);
match lua.load(config_path).exec_async().await {
Err(error) => {
println!("{error}");
Err(error)
}
result => result,
}?;
let automation: mlua::Table = lua.globals().get("automation")?;
let fulfillment_config: Option<mlua::Value> = automation.get("fulfillment")?;
if let Some(fulfillment_config) = fulfillment_config {
let entrypoint = Path::new(&config.entrypoint);
let fulfillment_config: mlua::Value = lua.load(entrypoint).eval_async().await?;
let fulfillment_config: FulfillmentConfig = lua.from_value(fulfillment_config)?;
debug!("automation.fulfillment = {fulfillment_config:?}");
fulfillment_config
} else {
return Err(anyhow!("Fulfillment is not configured"));
}
};
// Create google home fulfillment route
let fulfillment = Router::new().route("/google_home", post(fulfillment));

43
src/secret.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::str::from_utf8;
use config::{ConfigError, Source, Value, ValueKind};
#[derive(Debug, Clone, Default)]
pub struct EnvironmentSecretFile {}
const SUFFIX: &str = "__file";
const PREFIX: &str = concat!(std::env!("CARGO_PKG_NAME"), "__");
impl Source for EnvironmentSecretFile {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<config::Map<String, config::Value>, ConfigError> {
Ok(std::env::vars()
.flat_map(|(key, value): (String, String)| {
let key = key.to_lowercase();
if !key.starts_with(PREFIX) {
return None;
}
if !key.ends_with(SUFFIX) {
return None;
}
let suffix_length = key.len() - SUFFIX.len();
let key = key[PREFIX.len()..suffix_length].replace("__", ".");
if key.is_empty() {
return None;
}
let content = from_utf8(&std::fs::read(&value).unwrap())
.unwrap()
.to_owned();
Some((key, Value::new(Some(&value), ValueKind::String(content))))
})
.collect())
}
}