Compare commits

...

12 Commits

Author SHA1 Message Date
11d5d5db4d
Further work on automatically generating lua type definitions
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 6m25s
Build and deploy automation_rs / Build Docker image (push) Successful in 1m24s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-30 02:08:29 +02:00
2f494a7dd6
Started work on generating definitions 2024-04-29 21:55:03 +02:00
67ed13463a
Started work on reimplementing schedules
All checks were successful
Build and deploy automation_rs / Build Docker image (push) Successful in 40s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m22s
2024-04-29 04:55:39 +02:00
b16f2ae420
Fixed spelling mistakes 2024-04-29 04:55:39 +02:00
96f260492b
Moved last config items to lua + small cleanup 2024-04-29 04:55:30 +02:00
0b31b2e443
Fixed visibility of device configs
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m14s
Build and deploy automation_rs / Build Docker image (push) Successful in 51s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-29 03:03:42 +02:00
2b62aca78a
LuaDevice macro now uses LuaDeviceCreate trait to create devices from configs
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m53s
Build and deploy automation_rs / Build Docker image (push) Successful in 59s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-29 02:53:21 +02:00
40426862e5
mqtt client is now created in lua 2024-04-29 02:19:52 +02:00
c3bd05434c
DeviceManager no longer handles subscribing and filtering topics, each device has to do this themselves now 2024-04-29 02:12:47 +02:00
9385f27125
Improved how devices are created, ntfy and presence are now treated like any other device
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m30s
Build and deploy automation_rs / Build Docker image (push) Successful in 55s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-27 02:55:53 +02:00
8c327095fd
Moved schedule config from yml to lua 2024-04-26 23:16:39 +02:00
57596ae531
Set lua warning function 2024-04-26 21:54:55 +02:00
38 changed files with 1350 additions and 975 deletions

5
Cargo.lock generated
View File

@ -88,6 +88,7 @@ dependencies = [
"impl_cast",
"indexmap 2.0.0",
"mlua",
"once_cell",
"paste",
"pollster",
"regex",
@ -1108,9 +1109,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl-probe"

View File

@ -42,13 +42,8 @@ enum_dispatch = "0.3.12"
indexmap = { version = "2.0.0", features = ["serde"] }
serde_yaml = "0.9.27"
tokio-cron-scheduler = "0.9.4"
mlua = { version = "0.9.7", features = [
"lua54",
"vendored",
"macros",
"serialize",
"async",
] }
mlua = { version = "0.9.7", features = ["lua54", "vendored", "macros", "serialize", "async", "send"] }
once_cell = "1.19.0"
[patch.crates-io]
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }

View File

@ -1,7 +1,7 @@
FROM gcr.io/distroless/cc-debian12:nonroot
ENV AUTOMATION_CONFIG=/app/config.yml
COPY ./config/config.yml /app/config.yml
ENV AUTOMATION_CONFIG=/app/config.lua
COPY ./config.lua /app/config.lua
COPY ./build/automation /app/automation

View File

@ -1,11 +1,13 @@
mod lua_device;
mod lua_device_config;
mod lua_type_definition;
use lua_device::impl_lua_device_macro;
use lua_device_config::impl_lua_device_config_macro;
use lua_type_definition::impl_lua_type_definition;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(LuaDevice, attributes(config))]
#[proc_macro_derive(LuaDevice)]
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
@ -18,3 +20,10 @@ pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::T
impl_lua_device_config_macro(&ast).into()
}
#[proc_macro_derive(LuaTypeDefinition, attributes(device_config))]
pub fn lua_type_definition_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_lua_type_definition(&ast).into()
}

View File

@ -1,42 +1,38 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed};
use syn::DeriveInput;
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
// TODO: Handle errors properly
// This includes making sure one, and only one config is specified
let config = if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
.iter()
.find(|&field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("config"))
})
.map(|field| field.ty.clone())
.unwrap()
} else {
unimplemented!()
};
let gen = quote! {
impl #name {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?)
}
pub fn generate_lua_definition() -> String {
// TODO: Do not hardcode the name of the config type
let def = format!(
r#"--- @class {0}
{0} = {{}}
--- @param config {0}Config
--- @return WrappedDevice
function {0}.new(config) end
"#, stringify!(#name)
);
def
}
}
impl mlua::UserData for #name {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("new", |lua, config: mlua::Value| {
let config: #config = mlua::FromLua::from_lua(config, lua)?;
let config: Box<dyn crate::device_manager::DeviceConfig> = Box::new(config);
Ok(config)
methods.add_async_function("new", |lua, config: mlua::Value| async {
let config = mlua::FromLua::from_lua(config, lua)?;
// TODO: Using crate:: could cause issues
let device: #name = crate::devices::LuaDeviceCreate::create(config).await.map_err(mlua::ExternalError::into_lua_err)?;
Ok(crate::device_manager::WrappedDevice::new(Box::new(device)))
});
}
}

View File

@ -23,7 +23,7 @@ mod kw {
}
#[derive(Debug)]
enum Argument {
pub enum Argument {
Flatten {
_keyword: kw::flatten,
},
@ -107,8 +107,8 @@ impl Parse for Argument {
}
#[derive(Debug)]
struct Args {
args: Punctuated<Argument, Token![,]>,
pub(crate) struct Args {
pub(crate) args: Punctuated<Argument, Token![,]>,
}
impl Parse for Args {
@ -218,6 +218,21 @@ fn field_from_lua(field: &Field) -> TokenStream {
temp.into()
}
}),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => value,
[value] => value.to_owned(),
_ => {
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'from'")}
}
};
let value = match args
.iter()
.filter_map(|arg| match arg {
Argument::With { expr, .. } => Some(quote! {
{
let temp = #value;
@ -232,7 +247,7 @@ fn field_from_lua(field: &Field) -> TokenStream {
[] => value,
[value] => value.to_owned(),
_ => {
return quote_spanned! {field.span() => compile_error!("Only one of either 'from' or 'with' is allowed")}
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'with'")}
}
};

View File

@ -0,0 +1,137 @@
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{
AngleBracketedGenericArguments, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed,
PathArguments, Type, TypePath,
};
use crate::lua_device_config::{Args, Argument};
fn field_definition(field: &Field) -> TokenStream {
let (args, _): (Vec<_>, Vec<_>) = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("device_config") {
Some(attr.parse_args::<Args>().map(|args| args.args))
} else {
None
}
})
.partition_result();
let args: Vec<_> = args.into_iter().flatten().collect();
let field_name = if let Some(field_name) = args.iter().find_map(|arg| match arg {
Argument::Rename { ident, .. } => Some(ident),
_ => None,
}) {
field_name.value()
} else {
format!("{}", field.ident.clone().unwrap())
};
let mut optional = args
.iter()
.filter(|arg| matches!(arg, Argument::Default { .. } | Argument::DefaultExpr { .. }))
.count()
>= 1;
if args
.iter()
.filter(|arg| matches!(arg, Argument::Flatten { .. }))
.count()
>= 1
{
let field_type = &field.ty;
quote! {
#field_type::generate_lua_fields().as_str()
}
} else {
let path = if let Some(ty) = args.iter().find_map(|arg| match arg {
Argument::From { ty, .. } => Some(ty),
_ => None,
}) {
if let Type::Path(TypePath { path, .. }) = ty {
path.clone()
} else {
todo!();
}
} else if let Type::Path(TypePath { path, .. }) = field.ty.clone() {
path
} else {
todo!()
};
let seg = path.segments.first().unwrap();
let field_type = if seg.ident == "Option" {
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
seg.arguments.clone()
{
optional = true;
quote! { stringify!(#args) }
} else {
unreachable!("Option should always have angle brackets");
}
} else if seg.ident == "Vec" {
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
seg.arguments.clone()
{
optional = true;
quote! { stringify!(#args[]) }
} else {
unreachable!("Option should always have angle brackets");
}
} else {
quote! { stringify!(#path).replace(" :: ", "_") }
};
let mut format = "--- @field {} {}".to_string();
if optional {
format += "|nil";
}
format += "\n";
quote! {
format!(#format, #field_name, #field_type).as_str()
}
}
}
pub fn impl_lua_type_definition(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
let fields = if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
return quote_spanned! {ast.span() => compile_error!("This macro only works on named structs")};
};
let fields: Vec<_> = fields.iter().map(field_definition).collect();
let gen = quote! {
impl #name {
pub fn generate_lua_definition() -> String {
let mut def = format!("--- @class {}\n", stringify!(#name));
def += #name::generate_lua_fields().as_str();
def
}
pub fn generate_lua_fields() -> String {
let mut def = String::new();
#(def += #fields;)*
def
}
}
};
gen
}

View File

@ -1,5 +1,9 @@
print("Hello from lua")
automation.fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc",
}
local debug, value = pcall(automation.util.get_env, "DEBUG")
if debug and value ~= "true" then
debug = false
@ -13,157 +17,157 @@ local function mqtt_automation(topic)
return "automation/" .. topic
end
automation.device_manager:create(
"debug_bridge",
DebugBridge.new({
topic = mqtt_automation("debug"),
client = automation.mqtt_client,
})
)
local mqtt_client = automation.new_mqtt_client({
host = debug and "olympus.lan.huizinga.dev" or "mosquitto",
port = 8883,
client_name = debug and "automation-debug" or "automation_rs",
username = "mqtt",
password = automation.util.get_env("MQTT_PASSWORD"),
tls = debug and true or false,
})
automation.device_manager:add(Ntfy.new({
topic = automation.util.get_env("NTFY_TOPIC"),
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(Presence.new({
topic = "automation_dev/presence/+/#",
client = mqtt_client,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(DebugBridge.new({
identifier = "debug_bridge",
topic = mqtt_automation("debug"),
client = mqtt_client,
}))
local hue_ip = "10.0.0.146"
local hue_token = automation.util.get_env("HUE_TOKEN")
automation.device_manager:create(
"hue_bridge",
HueBridge.new({
ip = hue_ip,
login = hue_token,
flags = {
presence = 41,
darkness = 43,
},
})
)
automation.device_manager:add(HueBridge.new({
identifier = "hue_bridge",
ip = hue_ip,
login = hue_token,
flags = {
presence = 41,
darkness = 43,
},
}))
automation.device_manager:create(
"living_light_sensor",
LightSensor.new({
topic = mqtt_z2m("living/light"),
min = 22000,
max = 23500,
event_channel = automation.event_channel,
})
)
automation.device_manager:add(LightSensor.new({
identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"),
client = mqtt_client,
min = 22000,
max = 23500,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:create(
"living_zeus",
WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"),
mac_address = "30:9c:23:60:9c:13",
broadcast_ip = "10.0.0.255",
})
)
automation.device_manager:add(WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"),
client = mqtt_client,
mac_address = "30:9c:23:60:9c:13",
broadcast_ip = "10.0.0.255",
}))
local living_mixer = automation.device_manager:create("living_mixer", KasaOutlet.new({ ip = "10.0.0.49" }))
local living_speakers = automation.device_manager:create("living_speakers", KasaOutlet.new({ ip = "10.0.0.182" }))
local living_mixer = KasaOutlet.new({ identifier = "living_mixer", ip = "10.0.0.49" })
automation.device_manager:add(living_mixer)
local living_speakers = KasaOutlet.new({ identifier = "living_speakers", ip = "10.0.0.182" })
automation.device_manager:add(living_speakers)
automation.device_manager:create(
"living_audio",
AudioSetup.new({
topic = mqtt_z2m("living/remote"),
mixer = living_mixer,
speakers = living_speakers,
})
)
automation.device_manager:add(AudioSetup.new({
identifier = "living_audio",
topic = mqtt_z2m("living/remote"),
client = mqtt_client,
mixer = living_mixer,
speakers = living_speakers,
}))
automation.device_manager:create(
"kitchen_kettle",
IkeaOutlet.new({
outlet_type = "Kettle",
name = "Kettle",
room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"),
client = automation.mqtt_client,
timeout = debug and 5 or 300,
remotes = {
{ topic = mqtt_z2m("bedroom/remote") },
{ topic = mqtt_z2m("kitchen/remote") },
},
})
)
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Kettle",
name = "Kettle",
room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"),
client = mqtt_client,
timeout = debug and 5 or 300,
remotes = {
{ topic = mqtt_z2m("bedroom/remote") },
{ topic = mqtt_z2m("kitchen/remote") },
},
}))
automation.device_manager:create(
"batchroom_light",
IkeaOutlet.new({
outlet_type = "Light",
name = "Light",
room = "Bathroom",
topic = mqtt_z2m("batchroom/light"),
client = automation.mqtt_client,
timeout = debug and 60 or 45 * 60,
})
)
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Light",
name = "Light",
room = "Bathroom",
topic = mqtt_z2m("batchroom/light"),
client = mqtt_client,
timeout = debug and 60 or 45 * 60,
}))
automation.device_manager:create(
"bathroom_washer",
Washer.new({
topic = mqtt_z2m("batchroom/washer"),
threshold = 1,
event_channel = automation.event_channel,
})
)
automation.device_manager:add(Washer.new({
identifier = "bathroom_washer",
topic = mqtt_z2m("batchroom/washer"),
client = mqtt_client,
threshold = 1,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:create(
"workbench_charger",
IkeaOutlet.new({
outlet_type = "Charger",
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
client = automation.mqtt_client,
timeout = debug and 5 or 20 * 3600,
})
)
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Charger",
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
client = mqtt_client,
timeout = debug and 5 or 20 * 3600,
}))
automation.device_manager:create(
"workbench_outlet",
IkeaOutlet.new({
name = "Outlet",
room = "Workbench",
topic = mqtt_z2m("workbench/outlet"),
client = automation.mqtt_client,
})
)
automation.device_manager:add(IkeaOutlet.new({
name = "Outlet",
room = "Workbench",
topic = mqtt_z2m("workbench/outlet"),
client = mqtt_client,
}))
local hallway_lights = automation.device_manager:create(
"hallway_lights",
HueGroup.new({
ip = hue_ip,
login = hue_token,
group_id = 81,
scene_id = "3qWKxGVadXFFG4o",
timer_id = 1,
remotes = {
{ topic = mqtt_z2m("hallway/remote") },
},
})
)
local hallway_lights = automation.device_manager:add(HueGroup.new({
identifier = "hallway_lights",
ip = hue_ip,
login = hue_token,
group_id = 81,
scene_id = "3qWKxGVadXFFG4o",
timer_id = 1,
remotes = {
{ topic = mqtt_z2m("hallway/remote") },
},
client = mqtt_client,
}))
automation.device_manager:create(
"hallway_frontdoor",
ContactSensor.new({
topic = mqtt_z2m("hallway/frontdoor"),
client = automation.mqtt_client,
presence = {
topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60,
},
trigger = {
devices = { hallway_lights },
timeout = debug and 10 or 2 * 60,
},
})
)
automation.device_manager:add(ContactSensor.new({
identifier = "hallway_frontdoor",
topic = mqtt_z2m("hallway/frontdoor"),
client = mqtt_client,
presence = {
topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60,
},
trigger = {
devices = { hallway_lights },
timeout = debug and 10 or 2 * 60,
},
}))
automation.device_manager:create(
"bedroom_air_filter",
AirFilter.new({
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = automation.mqtt_client,
})
)
local bedroom_air_filter = AirFilter.new({
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = mqtt_client,
})
automation.device_manager:add(bedroom_air_filter)
automation.device_manager:schedule("0/1 * * * * *", function()
print("Device: " .. bedroom_air_filter:get_id())
end)

View File

@ -1,25 +0,0 @@
openid:
base_url: "https://login.huizinga.dev/api/oidc"
mqtt:
host: "mosquitto"
port: 8883
client_name: "automation_rs"
username: "mqtt"
password: "${MQTT_PASSWORD}"
ntfy:
topic: "${NTFY_TOPIC}"
presence:
topic: "automation/presence/+/#"
# Run the air filter everyday for 19:00 to 20:00
schedule:
0 0 19 * * *:
on:
- "bedroom_air_filter"
0 0 20 * * *:
off:
- "bedroom_air_filter"

View File

@ -1,25 +0,0 @@
openid:
base_url: "https://login.huizinga.dev/api/oidc"
mqtt:
host: "olympus.lan.huizinga.dev"
port: 8883
client_name: "automation-zeus"
username: "mqtt"
password: "${MQTT_PASSWORD}"
tls: true
ntfy:
topic: "${NTFY_TOPIC}"
presence:
topic: "automation_dev/presence/+/#"
schedule:
# 0/30 * * * * *:
# on:
# - *outlet
#
# 15/30 * * * * *:
# off:
# - *outlet

View File

@ -0,0 +1,40 @@
--- @meta
--- @class WrappedDevice
WrappedDevice = {}
--- @return string
function WrappedDevice:get_id() end
--- @class WrappedAsyncClient
--- @class EventChannel
--- @return EventChannel
function automation.device_manager:event_channel() end
automation = {}
automation.device_manager = {}
--- @param device WrappedDevice
function automation.device_manager:add(device) end
--- @param when string
--- @param func function
function automation.device_manager:schedule(when, func) end
automation.util = {}
--- @param env string
--- @return string
function automation.util.get_env(env) end
--- @class Fulfillment
--- @field openid_url string|nil
automation.fulfillment = {}
--- @class MqttConfig
--- @param config MqttConfig
--- @return WrappedAsyncClient
function automation.new_mqtt_client(config) end
--- TODO: Generate this automatically
--- @alias OutletType "Outlet"|"Kettle"|"Charger"|"Light"
--- @alias TriggerDevicesHelper WrappedDevice[]

183
definitions/generated.lua Normal file
View File

@ -0,0 +1,183 @@
-- WARN: This file is automatically generated, do not manually edit
---@meta
--- @class MqttDeviceConfig
--- @field topic String
--- @class AirFilter
AirFilter = {}
--- @param config AirFilterConfig
--- @return WrappedDevice
function AirFilter.new(config) end
--- @class AirFilterConfig
--- @field name String
--- @field room String|nil
--- @field topic String
--- @field client WrappedAsyncClient
--- @class AudioSetup
AudioSetup = {}
--- @param config AudioSetupConfig
--- @return WrappedDevice
function AudioSetup.new(config) end
--- @class AudioSetupConfig
--- @field identifier String
--- @field topic String
--- @field mixer WrappedDevice
--- @field speakers WrappedDevice
--- @field client WrappedAsyncClient
--- @class ContactSensor
ContactSensor = {}
--- @param config ContactSensorConfig
--- @return WrappedDevice
function ContactSensor.new(config) end
--- @class ContactSensorConfig
--- @field identifier String
--- @field topic String
--- @field presence PresenceDeviceConfig|nil
--- @field trigger TriggerConfig|nil
--- @field client WrappedAsyncClient
--- @class PresenceDeviceConfig
--- @field topic String
--- @field timeout u64
--- @class TriggerConfig
--- @field devices TriggerDevicesHelper
--- @field timeout u64|nil
--- @class DebugBridge
DebugBridge = {}
--- @param config DebugBridgeConfig
--- @return WrappedDevice
function DebugBridge.new(config) end
--- @class DebugBridgeConfig
--- @field identifier String
--- @field topic String
--- @field client WrappedAsyncClient
--- @class HueBridge
HueBridge = {}
--- @param config HueBridgeConfig
--- @return WrappedDevice
function HueBridge.new(config) end
--- @class HueBridgeConfig
--- @field identifier String
--- @field ip Ipv4Addr
--- @field login String
--- @field flags FlagIDs
--- @class FlagIDs
--- @field presence isize
--- @field darkness isize
--- @class HueGroup
HueGroup = {}
--- @param config HueGroupConfig
--- @return WrappedDevice
function HueGroup.new(config) end
--- @class HueGroupConfig
--- @field identifier String
--- @field ip Ipv4Addr
--- @field login String
--- @field group_id isize
--- @field timer_id isize
--- @field scene_id String
--- @field remotes MqttDeviceConfig []|nil
--- @field client WrappedAsyncClient
--- @class IkeaOutlet
IkeaOutlet = {}
--- @param config IkeaOutletConfig
--- @return WrappedDevice
function IkeaOutlet.new(config) end
--- @class IkeaOutletConfig
--- @field name String
--- @field room String|nil
--- @field topic String
--- @field outlet_type OutletType|nil
--- @field timeout u64|nil
--- @field remotes MqttDeviceConfig []|nil
--- @field client WrappedAsyncClient
--- @class KasaOutlet
KasaOutlet = {}
--- @param config KasaOutletConfig
--- @return WrappedDevice
function KasaOutlet.new(config) end
--- @class KasaOutletConfig
--- @field identifier String
--- @field ip Ipv4Addr
--- @class LightSensor
LightSensor = {}
--- @param config LightSensorConfig
--- @return WrappedDevice
function LightSensor.new(config) end
--- @class LightSensorConfig
--- @field identifier String
--- @field topic String
--- @field min isize
--- @field max isize
--- @field event_channel EventChannel
--- @field client WrappedAsyncClient
--- @class Ntfy
Ntfy = {}
--- @param config NtfyConfig
--- @return WrappedDevice
function Ntfy.new(config) end
--- @class NtfyConfig
--- @field url String|nil
--- @field topic String
--- @field event_channel EventChannel
--- @class Presence
Presence = {}
--- @param config PresenceConfig
--- @return WrappedDevice
function Presence.new(config) end
--- @class PresenceConfig
--- @field topic String
--- @field event_channel EventChannel
--- @field client WrappedAsyncClient
--- @class WakeOnLAN
WakeOnLAN = {}
--- @param config WakeOnLANConfig
--- @return WrappedDevice
function WakeOnLAN.new(config) end
--- @class WakeOnLANConfig
--- @field name String
--- @field room String|nil
--- @field topic String
--- @field mac_address MacAddress
--- @field broadcast_ip Ipv4Addr|nil
--- @field client WrappedAsyncClient
--- @class Washer
Washer = {}
--- @param config WasherConfig
--- @return WrappedDevice
function Washer.new(config) end
--- @class WasherConfig
--- @field identifier String
--- @field topic String
--- @field threshold f32
--- @field event_channel EventChannel
--- @field client WrappedAsyncClient

10
definitions/rust.lua Normal file
View File

@ -0,0 +1,10 @@
--- @meta
--- @alias String string
--- @alias u64 number
--- @alias isize number
--- @alias f32 number
--- @alias Ipv4Addr string
--- @alias MacAddress string

View File

@ -46,10 +46,10 @@ where
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
fn get_device_type(&self) -> Type;
fn get_device_name(&self) -> Name;
fn get_id(&self) -> &str;
fn get_id(&self) -> String;
fn is_online(&self) -> bool;
// Default values that can optionally be overriden
// Default values that can optionally be overridden
fn will_report_state(&self) -> bool {
false
}
@ -63,7 +63,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
async fn sync(&self) -> response::sync::Device {
let name = self.get_device_name();
let mut device =
response::sync::Device::new(self.get_id(), &name.name, self.get_device_type());
response::sync::Device::new(&self.get_id(), &name.name, self.get_device_type());
device.name = name;
device.will_report_state = self.will_report_state();

View File

@ -17,7 +17,7 @@ pub struct GoogleHome {
}
#[derive(Debug, Error)]
pub enum FullfillmentError {
pub enum FulfillmentError {
#[error("Expected at least one ResponsePayload")]
ExpectedOnePayload,
}
@ -33,7 +33,7 @@ impl GoogleHome {
&self,
request: Request,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
) -> Result<Response, FullfillmentError> {
) -> Result<Response, FulfillmentError> {
// TODO: What do we do if we actually get more then one thing in the input array, right now
// we only respond to the first thing
let intent = request.inputs.into_iter().next();
@ -54,7 +54,7 @@ impl GoogleHome {
payload
.await
.ok_or(FullfillmentError::ExpectedOnePayload)
.ok_or(FulfillmentError::ExpectedOnePayload)
.map(|payload| Response::new(&request.request_id, payload))
}

View File

@ -2,7 +2,7 @@
#![feature(specialization)]
#![feature(let_chains)]
pub mod device;
mod fullfillment;
mod fulfillment;
mod request;
mod response;
@ -13,6 +13,6 @@ pub mod traits;
pub mod types;
pub use device::GoogleHomeDevice;
pub use fullfillment::{FullfillmentError, GoogleHome};
pub use fulfillment::{FulfillmentError, GoogleHome};
pub use request::Request;
pub use response::Response;

View File

@ -6,11 +6,6 @@ use serde::Deserialize;
use crate::error::{ApiError, ApiErrorJson};
#[derive(Debug, Clone, Deserialize)]
pub struct OpenIDConfig {
pub base_url: String,
}
#[derive(Debug, Deserialize)]
pub struct User {
pub preferred_username: String,
@ -19,18 +14,18 @@ pub struct User {
#[async_trait]
impl<S> FromRequestParts<S> for User
where
OpenIDConfig: FromRef<S>,
String: FromRef<S>,
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Get the state
let openid = OpenIDConfig::from_ref(state);
let openid_url = String::from_ref(state);
// Create a request to the auth server
// TODO: Do some discovery to find the correct url for this instead of assuming
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid.base_url));
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url));
// Add auth header to the request if it exists
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {

View File

@ -0,0 +1,57 @@
use automation::config::MqttDeviceConfig;
use automation::devices::{
AirFilter, AirFilterConfig, AudioSetup, AudioSetupConfig, ContactSensor, ContactSensorConfig,
DebugBridge, DebugBridgeConfig, FlagIDs, HueBridge, HueBridgeConfig, HueGroup, HueGroupConfig,
IkeaOutlet, IkeaOutletConfig, KasaOutlet, KasaOutletConfig, LightSensor, LightSensorConfig,
Ntfy, NtfyConfig, Presence, PresenceConfig, PresenceDeviceConfig, TriggerConfig, WakeOnLAN,
WakeOnLANConfig, Washer, WasherConfig,
};
fn main() {
println!("-- WARN: This file is automatically generated, do not manually edit\n");
println!("---@meta");
println!("{}", MqttDeviceConfig::generate_lua_definition());
println!("{}", AirFilter::generate_lua_definition());
println!("{}", AirFilterConfig::generate_lua_definition());
println!("{}", AudioSetup::generate_lua_definition());
println!("{}", AudioSetupConfig::generate_lua_definition());
println!("{}", ContactSensor::generate_lua_definition());
println!("{}", ContactSensorConfig::generate_lua_definition());
println!("{}", PresenceDeviceConfig::generate_lua_definition());
println!("{}", TriggerConfig::generate_lua_definition());
println!("{}", DebugBridge::generate_lua_definition());
println!("{}", DebugBridgeConfig::generate_lua_definition());
println!("{}", HueBridge::generate_lua_definition());
println!("{}", HueBridgeConfig::generate_lua_definition());
println!("{}", FlagIDs::generate_lua_definition());
println!("{}", HueGroup::generate_lua_definition());
println!("{}", HueGroupConfig::generate_lua_definition());
println!("{}", IkeaOutlet::generate_lua_definition());
println!("{}", IkeaOutletConfig::generate_lua_definition());
println!("{}", KasaOutlet::generate_lua_definition());
println!("{}", KasaOutletConfig::generate_lua_definition());
println!("{}", LightSensor::generate_lua_definition());
println!("{}", LightSensorConfig::generate_lua_definition());
println!("{}", Ntfy::generate_lua_definition());
println!("{}", NtfyConfig::generate_lua_definition());
println!("{}", Presence::generate_lua_definition());
println!("{}", PresenceConfig::generate_lua_definition());
println!("{}", WakeOnLAN::generate_lua_definition());
println!("{}", WakeOnLANConfig::generate_lua_definition());
println!("{}", Washer::generate_lua_definition());
println!("{}", WasherConfig::generate_lua_definition());
}

View File

@ -1,28 +1,9 @@
use std::fs;
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use regex::{Captures, Regex};
use automation_macro::LuaTypeDefinition;
use rumqttc::{MqttOptions, Transport};
use serde::{Deserialize, Deserializer};
use tracing::debug;
use crate::auth::OpenIDConfig;
use crate::devices::PresenceConfig;
use crate::error::{ConfigParseError, MissingEnv};
use crate::schedule::Schedule;
#[derive(Debug, Deserialize)]
pub struct Config {
pub openid: OpenIDConfig,
#[serde(deserialize_with = "deserialize_mqtt_options")]
pub mqtt: MqttOptions,
#[serde(default)]
pub fullfillment: FullfillmentConfig,
pub ntfy: Option<NtfyConfig>,
pub presence: PresenceConfig,
pub schedule: Schedule,
}
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct MqttConfig {
@ -49,90 +30,46 @@ impl From<MqttConfig> for MqttOptions {
}
}
fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result<MqttOptions, D::Error>
where
D: Deserializer<'de>,
{
Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?))
}
#[derive(Debug, Deserialize)]
pub struct FullfillmentConfig {
#[serde(default = "default_fullfillment_ip")]
pub struct FulfillmentConfig {
pub openid_url: String,
#[serde(default = "default_fulfillment_ip")]
pub ip: Ipv4Addr,
#[serde(default = "default_fullfillment_port")]
#[serde(default = "default_fulfillment_port")]
pub port: u16,
}
impl From<FullfillmentConfig> for SocketAddr {
fn from(fullfillment: FullfillmentConfig) -> Self {
(fullfillment.ip, fullfillment.port).into()
impl From<FulfillmentConfig> for SocketAddr {
fn from(fulfillment: FulfillmentConfig) -> Self {
(fulfillment.ip, fulfillment.port).into()
}
}
impl Default for FullfillmentConfig {
fn default() -> Self {
Self {
ip: default_fullfillment_ip(),
port: default_fullfillment_port(),
}
}
}
fn default_fullfillment_ip() -> Ipv4Addr {
fn default_fulfillment_ip() -> Ipv4Addr {
[0, 0, 0, 0].into()
}
fn default_fullfillment_port() -> u16 {
fn default_fulfillment_port() -> u16 {
7878
}
#[derive(Debug, Deserialize)]
pub struct NtfyConfig {
#[serde(default = "default_ntfy_url")]
pub url: String,
pub topic: String,
}
fn default_ntfy_url() -> String {
"https://ntfy.sh".into()
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
pub struct InfoConfig {
pub name: String,
pub room: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
impl InfoConfig {
pub fn identifier(&self) -> String {
(if let Some(room) = &self.room {
room.to_ascii_lowercase().replace(' ', "_") + "_"
} else {
String::new()
}) + &self.name.to_ascii_lowercase().replace(' ', "_")
}
}
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
pub struct MqttDeviceConfig {
pub topic: String,
}
impl Config {
pub fn parse_file(filename: &str) -> Result<Self, ConfigParseError> {
debug!("Loading config: {filename}");
let file = fs::read_to_string(filename)?;
// Substitute in environment variables
let re = Regex::new(r"\$\{(.*)\}").expect("Regex should be valid");
let mut missing = MissingEnv::new();
let file = re.replace_all(&file, |caps: &Captures| {
let key = caps.get(1).expect("Capture group should exist").as_str();
debug!("Substituting '{key}' in config");
match std::env::var(key) {
Ok(value) => value,
Err(_) => {
missing.add_missing(key);
"".into()
}
}
});
missing.has_missing()?;
let config: Config = serde_yaml::from_str(&file)?;
Ok(config)
}
}

View File

@ -2,33 +2,21 @@ use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use async_trait::async_trait;
use enum_dispatch::enum_dispatch;
use futures::future::join_all;
use google_home::traits::OnOff;
use mlua::FromLua;
use rumqttc::{matches, AsyncClient, QoS};
use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, error, instrument, trace};
use tracing::{debug, instrument, trace};
use crate::devices::{As, Device};
use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::schedule::{Action, Schedule};
#[async_trait]
#[enum_dispatch]
pub trait DeviceConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError>;
}
impl mlua::UserData for Box<dyn DeviceConfig> {}
use crate::LUA;
#[derive(Debug, FromLua, Clone)]
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
impl WrappedDevice {
fn new(device: Box<dyn Device>) -> Self {
pub fn new(device: Box<dyn Device>) -> Self {
Self(Arc::new(RwLock::new(device)))
}
}
@ -46,25 +34,31 @@ impl DerefMut for WrappedDevice {
&mut self.0
}
}
impl mlua::UserData for WrappedDevice {}
impl mlua::UserData for WrappedDevice {
fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method("get_id", |_lua, this, _: ()| async {
Ok(crate::devices::Device::get_id(this.0.read().await.as_ref()))
});
}
}
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct DeviceManager {
devices: Arc<RwLock<DeviceMap>>,
client: AsyncClient,
event_channel: EventChannel,
scheduler: JobScheduler,
}
impl DeviceManager {
pub fn new(client: AsyncClient) -> Self {
pub async fn new() -> Self {
let (event_channel, mut event_rx) = EventChannel::new();
let device_manager = Self {
devices: Arc::new(RwLock::new(HashMap::new())),
client,
event_channel,
scheduler: JobScheduler::new().await.unwrap(),
};
tokio::spawn({
@ -80,81 +74,17 @@ impl DeviceManager {
}
});
device_manager.scheduler.start().await.unwrap();
device_manager
}
// TODO: This function is currently extremely cursed...
pub async fn add_schedule(&self, schedule: Schedule) {
let sched = JobScheduler::new().await.unwrap();
for (when, actions) in schedule {
let manager = self.clone();
sched
.add(
Job::new_async(when.as_str(), move |_uuid, _l| {
let actions = actions.clone();
let manager = manager.clone();
Box::pin(async move {
for (action, targets) in actions {
for target in targets {
let device = manager.get(&target).await.unwrap();
match action {
Action::On => {
As::<dyn OnOff>::cast_mut(
device.write().await.as_mut(),
)
.unwrap()
.set_on(true)
.await
.unwrap();
}
Action::Off => {
As::<dyn OnOff>::cast_mut(
device.write().await.as_mut(),
)
.unwrap()
.set_on(false)
.await
.unwrap();
}
}
}
}
})
})
.unwrap(),
)
.await
.unwrap();
}
sched.start().await.unwrap();
}
pub async fn add(&self, device: Box<dyn Device>) -> WrappedDevice {
let id = device.get_id().into();
pub async fn add(&self, device: &WrappedDevice) {
let id = device.read().await.get_id();
debug!(id, "Adding device");
// If the device listens to mqtt, subscribe to the topics
if let Some(device) = As::<dyn OnMqtt>::cast(device.as_ref()) {
for topic in device.topics() {
trace!(id, topic, "Subscribing to topic");
if let Err(err) = self.client.subscribe(topic, QoS::AtLeastOnce).await {
// NOTE: Pretty sure that this can only happen if the mqtt client if no longer
// running
error!(id, topic, "Failed to subscribe to topic: {err}");
}
}
}
// Wrap the device
let device = WrappedDevice::new(device);
self.devices.write().await.insert(id, device.0.clone());
device
}
pub fn event_channel(&self) -> EventChannel {
@ -185,15 +115,16 @@ impl DeviceManager {
let mut device = device.write().await;
let device = device.as_mut();
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
let subscribed = device
.topics()
.iter()
.any(|topic| matches(&message.topic, topic));
if subscribed {
trace!(id, "Handling");
device.on_mqtt(message).await;
}
// let subscribed = device
// .topics()
// .iter()
// .any(|topic| matches(&message.topic, topic));
//
// if subscribed {
trace!(id, "Handling");
device.on_mqtt(message).await;
trace!(id, "Done");
// }
}
}
});
@ -208,6 +139,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
trace!(id, "Handling");
device.on_darkness(dark).await;
trace!(id, "Done");
}
});
@ -221,6 +153,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
trace!(id, "Handling");
device.on_presence(presence).await;
trace!(id, "Done");
}
});
@ -236,6 +169,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
trace!(id, "Handling");
device.on_notification(notification).await;
trace!(id, "Done");
}
}
});
@ -248,19 +182,47 @@ impl DeviceManager {
impl mlua::UserData for DeviceManager {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method("add", |_lua, this, device: WrappedDevice| async move {
this.add(&device).await;
Ok(())
});
methods.add_async_method(
"create",
|_lua, this, (identifier, config): (String, mlua::Value)| async move {
// TODO: Handle the error here properly
let config: Box<dyn DeviceConfig> = config.as_userdata().unwrap().take()?;
"schedule",
|lua, this, (schedule, f): (String, mlua::Function)| async move {
debug!("schedule = {schedule}");
let uuid = this
.scheduler
.add(
Job::new_async(schedule.as_str(), |uuid, _lock| {
Box::pin(async move {
let lua = LUA.lock().await;
let f: mlua::Function =
lua.named_registry_value(uuid.to_string().as_str()).unwrap();
let device = config
.create(&identifier)
f.call::<_, ()>(()).unwrap();
})
})
.unwrap(),
)
.await
.map_err(mlua::ExternalError::into_lua_err)?;
.unwrap();
Ok(this.add(device).await)
// Store the function in the registry
lua.set_named_registry_value(uuid.to_string().as_str(), f)
.unwrap();
Ok(())
},
)
);
// methods.add_async_method("add_schedule", |lua, this, schedule| async {
// let schedule = lua.from_value(schedule)?;
// this.add_schedule(schedule).await;
// Ok(())
// });
methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel()))
}
}

View File

@ -1,51 +1,32 @@
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use google_home::device::Name;
use google_home::errors::ErrorCode;
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
use google_home::types::Type;
use google_home::GoogleHomeDevice;
use rumqttc::Publish;
use tracing::{debug, error, warn};
use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct AirFilterConfig {
#[device_config(flatten)]
info: InfoConfig,
pub info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for AirFilterConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = AirFilter {
identifier: identifier.into(),
config: self.clone(),
last_known_state: AirFilterState {
state: AirFilterFanState::Off,
humidity: 0.0,
},
};
Ok(Box::new(device))
}
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct AirFilter {
identifier: String,
#[config]
config: AirFilterConfig,
last_known_state: AirFilterState,
@ -60,7 +41,7 @@ impl AirFilter {
self.config
.client
.publish(
topic.clone(),
&topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&message).unwrap(),
@ -71,23 +52,46 @@ impl AirFilter {
}
}
#[async_trait]
impl LuaDeviceCreate for AirFilter {
type Config = AirFilterConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up AirFilter");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
last_known_state: AirFilterState {
state: AirFilterFanState::Off,
humidity: 0.0,
},
})
}
}
impl Device for AirFilter {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.info.identifier()
}
}
#[async_trait]
impl OnMqtt for AirFilter {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let state = match AirFilterState::try_from(message) {
Ok(state) => state,
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
@ -96,7 +100,7 @@ impl OnMqtt for AirFilter {
return;
}
debug!(id = self.identifier, "Updating state to {state:?}");
debug!(id = Device::get_id(self), "Updating state to {state:?}");
self.last_known_state = state;
}
@ -111,7 +115,7 @@ impl GoogleHomeDevice for AirFilter {
Name::new(&self.config.info.name)
}
fn get_id(&self) -> &str {
fn get_id(&self) -> String {
Device::get_id(self)
}

View File

@ -1,75 +1,82 @@
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use google_home::traits::OnOff;
use tracing::{debug, error, trace, warn};
use super::Device;
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::device_manager::WrappedDevice;
use crate::devices::As;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct AudioSetupConfig {
pub identifier: String,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
mixer: WrappedDevice,
pub mixer: WrappedDevice,
#[device_config(from_lua)]
speakers: WrappedDevice,
pub speakers: WrappedDevice,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for AudioSetupConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up AudioSetup");
let mixer_id = self.mixer.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(self.mixer.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
}
let speakers_id = self.speakers.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(self.speakers.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
}
let device = AudioSetup {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
}
// TODO: We need a better way to store the children devices
#[derive(Debug, LuaDevice)]
pub struct AudioSetup {
identifier: String,
#[config]
config: AudioSetupConfig,
}
#[async_trait]
impl LuaDeviceCreate for AudioSetup {
type Config = AudioSetupConfig;
type Error = DeviceConfigError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup");
let mixer_id = config.mixer.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(config.mixer.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
}
let speakers_id = config.speakers.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(config.speakers.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(AudioSetup { config })
}
}
impl Device for AudioSetup {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
#[async_trait]
impl OnMqtt for AudioSetup {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return;
}
};
@ -118,7 +125,7 @@ impl OnPresence for AudioSetup {
) {
// Turn off the audio setup when we leave the house
if !presence {
debug!(id = self.identifier, "Turning devices off");
debug!(id = self.config.identifier, "Turning devices off");
speakers.set_on(false).await.unwrap();
mixer.set_on(false).await.unwrap();
}

View File

@ -1,15 +1,15 @@
use std::time::Duration;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use google_home::traits::OnOff;
use mlua::FromLua;
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn};
use super::Device;
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::device_manager::WrappedDevice;
use crate::devices::{As, DEFAULT_PRESENCE};
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
@ -18,11 +18,11 @@ use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
// NOTE: If we add more presence devices we might need to move this out of here
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct PresenceDeviceConfig {
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(with(Duration::from_secs))]
#[device_config(from(u64), with(Duration::from_secs))]
pub timeout: Duration,
}
@ -41,33 +41,46 @@ impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
}
}
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct TriggerConfig {
#[device_config(from_lua, from(TriggerDevicesHelper))]
devices: Vec<(WrappedDevice, bool)>,
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
pub devices: Vec<(WrappedDevice, bool)>,
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct ContactSensorConfig {
pub identifier: String,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
presence: Option<PresenceDeviceConfig>,
pub presence: Option<PresenceDeviceConfig>,
#[device_config(from_lua)]
trigger: Option<TriggerConfig>,
pub trigger: Option<TriggerConfig>,
#[device_config(from_lua)]
client: WrappedAsyncClient,
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct ContactSensor {
config: ContactSensorConfig,
overall_presence: bool,
is_closed: bool,
handle: Option<JoinHandle<()>>,
}
#[async_trait]
impl DeviceConfig for ContactSensorConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up ContactSensor");
impl LuaDeviceCreate for ContactSensor {
type Config = ContactSensorConfig;
type Error = DeviceConfigError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up ContactSensor");
// Make sure the devices implement the required traits
if let Some(trigger) = &self.trigger {
if let Some(trigger) = &config.trigger {
for (device, _) in &trigger.devices {
let id = device.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
@ -81,32 +94,23 @@ impl DeviceConfig for ContactSensorConfig {
}
}
let device = ContactSensor {
identifier: identifier.into(),
config: self.clone(),
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config: config.clone(),
overall_presence: DEFAULT_PRESENCE,
is_closed: true,
handle: None,
};
Ok(Box::new(device))
})
}
}
#[derive(Debug, LuaDevice)]
pub struct ContactSensor {
identifier: String,
#[config]
config: ContactSensorConfig,
overall_presence: bool,
is_closed: bool,
handle: Option<JoinHandle<()>>,
}
impl Device for ContactSensor {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
@ -119,15 +123,18 @@ impl OnPresence for ContactSensor {
#[async_trait]
impl OnMqtt for ContactSensor {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let is_closed = match ContactMessage::try_from(message) {
Ok(state) => state.is_closed(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return;
}
};
@ -136,7 +143,7 @@ impl OnMqtt for ContactSensor {
return;
}
debug!(id = self.identifier, "Updating state to {is_closed}");
debug!(id = self.config.identifier, "Updating state to {is_closed}");
self.is_closed = is_closed;
if let Some(trigger) = &mut self.config.trigger {
@ -188,7 +195,7 @@ impl OnMqtt for ContactSensor {
self.config
.client
.publish(
presence.mqtt.topic.clone(),
&presence.mqtt.topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
@ -205,7 +212,7 @@ impl OnMqtt for ContactSensor {
} else {
// Once the door is closed again we start a timeout for removing the presence
let client = self.config.client.clone();
let id = self.identifier.clone();
let id = self.config.identifier.clone();
let timeout = presence.timeout;
let topic = presence.mqtt.topic.clone();
self.handle = Some(tokio::spawn(async move {
@ -213,7 +220,7 @@ impl OnMqtt for ContactSensor {
tokio::time::sleep(timeout).await;
debug!(id, "Removing door device!");
client
.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "")
.publish(&topic, rumqttc::QoS::AtLeastOnce, false, "")
.await
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
.ok();

View File

@ -1,45 +1,44 @@
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use tracing::warn;
use std::convert::Infallible;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use tracing::{trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence};
use crate::messages::{DarknessMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, LuaDeviceConfig, Clone)]
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
pub struct DebugBridgeConfig {
pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for DebugBridgeConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = DebugBridge {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct DebugBridge {
identifier: String,
#[config]
config: DebugBridgeConfig,
}
#[async_trait]
impl LuaDeviceCreate for DebugBridge {
type Config = DebugBridgeConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up DebugBridge");
Ok(Self { config })
}
}
impl Device for DebugBridge {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}

View File

@ -1,13 +1,13 @@
use std::net::SocketAddr;
use std::convert::Infallible;
use std::net::{Ipv4Addr, SocketAddr};
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn};
use crate::device_manager::DeviceConfig;
use super::LuaDeviceCreate;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence};
#[derive(Debug)]
@ -16,36 +16,23 @@ pub enum Flag {
Darkness,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
pub struct FlagIDs {
pub presence: isize,
pub darkness: isize,
presence: isize,
darkness: isize,
}
#[derive(Debug, LuaDeviceConfig, Clone)]
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
pub struct HueBridgeConfig {
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub identifier: String,
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String,
pub flags: FlagIDs,
}
#[async_trait]
impl DeviceConfig for HueBridgeConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueBridge {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
}
#[derive(Debug, LuaDevice)]
pub struct HueBridge {
identifier: String,
#[config]
config: HueBridgeConfig,
}
@ -54,6 +41,17 @@ struct FlagMessage {
flag: bool,
}
#[async_trait]
impl LuaDeviceCreate for HueBridge {
type Config = HueBridgeConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Infallible> {
trace!(id = config.identifier, "Setting up HueBridge");
Ok(Self { config })
}
}
impl HueBridge {
pub async fn set_flag(&self, flag: Flag, value: bool) {
let flag_id = match flag {
@ -88,8 +86,8 @@ impl HueBridge {
}
impl Device for HueBridge {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}

View File

@ -1,25 +1,25 @@
use std::net::SocketAddr;
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use rumqttc::Publish;
use tracing::{debug, error, warn};
use rumqttc::{Publish, SubscribeFilter};
use tracing::{debug, error, trace, warn};
use super::Device;
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct HueGroupConfig {
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub identifier: String,
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String,
pub group_id: isize,
@ -27,28 +27,38 @@ pub struct HueGroupConfig {
pub scene_id: String,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
}
#[async_trait]
impl DeviceConfig for HueGroupConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueGroup {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct HueGroup {
identifier: String,
#[config]
config: HueGroupConfig,
}
// Couple of helper function to get the correct urls
#[async_trait]
impl LuaDeviceCreate for HueGroup {
type Config = HueGroupConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
Ok(Self { config })
}
}
impl HueGroup {
fn url_base(&self) -> String {
format!("http://{}/api/{}", self.config.addr, self.config.login)
@ -68,26 +78,30 @@ impl HueGroup {
}
impl Device for HueGroup {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
#[async_trait]
impl OnMqtt for HueGroup {
fn topics(&self) -> Vec<&str> {
self.config
async fn on_mqtt(&mut self, message: Publish) {
if !self
.config
.remotes
.iter()
.map(|mqtt| mqtt.topic.as_str())
.collect()
}
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
return;
}
async fn on_mqtt(&mut self, message: Publish) {
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return;
}
};
@ -126,10 +140,13 @@ impl OnOff for HueGroup {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(id = self.identifier, "Status code is not success: {status}");
warn!(
id = self.config.identifier,
"Status code is not success: {status}"
);
}
}
Err(err) => error!(id = self.identifier, "Error: {err}"),
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
}
Ok(())
@ -145,13 +162,19 @@ impl OnOff for HueGroup {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(id = self.identifier, "Status code is not success: {status}");
warn!(
id = self.config.identifier,
"Status code is not success: {status}"
);
}
let on = match res.json::<message::Info>().await {
Ok(info) => info.any_on(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
// TODO: Error code
return Ok(false);
}
@ -159,7 +182,7 @@ impl OnOff for HueGroup {
return Ok(on);
}
Err(err) => error!(id = self.identifier, "Error: {err}"),
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
}
Ok(false)

View File

@ -2,20 +2,19 @@ use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff};
use google_home::types::Type;
use google_home::{device, GoogleHomeDevice};
use rumqttc::{matches, Publish};
use rumqttc::{matches, Publish, SubscribeFilter};
use serde::Deserialize;
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
@ -29,48 +28,25 @@ pub enum OutletType {
Light,
}
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct IkeaOutletConfig {
#[device_config(flatten)]
info: InfoConfig,
pub info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
pub mqtt: MqttDeviceConfig,
#[device_config(default(OutletType::Outlet))]
outlet_type: OutletType,
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
timeout: Option<Duration>,
pub outlet_type: OutletType,
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for IkeaOutletConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
room = self.info.room,
"Setting up IkeaOutlet"
);
let device = IkeaOutlet {
identifier: identifier.into(),
config: self.clone(),
last_known_state: false,
handle: None,
};
Ok(Box::new(device))
}
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct IkeaOutlet {
identifier: String,
#[config]
config: IkeaOutletConfig,
last_known_state: bool,
@ -84,7 +60,7 @@ async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
// TODO: Handle potential errors here
client
.publish(
topic.clone(),
&topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&message).unwrap(),
@ -94,27 +70,45 @@ async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
.ok();
}
#[async_trait]
impl LuaDeviceCreate for IkeaOutlet {
type Config = IkeaOutletConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
last_known_state: false,
handle: None,
})
}
}
impl Device for IkeaOutlet {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.info.identifier()
}
}
#[async_trait]
impl OnMqtt for IkeaOutlet {
fn topics(&self) -> Vec<&str> {
let mut topics: Vec<_> = self
.config
.remotes
.iter()
.map(|mqtt| mqtt.topic.as_str())
.collect();
topics.push(&self.config.mqtt.topic);
topics
}
async fn on_mqtt(&mut self, message: Publish) {
// Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) {
@ -122,7 +116,7 @@ impl OnMqtt for IkeaOutlet {
let state = match OnOffMessage::try_from(message) {
Ok(state) => state.state(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
@ -135,18 +129,23 @@ impl OnMqtt for IkeaOutlet {
// Abort any timer that is currently running
self.stop_timeout().await.unwrap();
debug!(id = self.identifier, "Updating state to {state}");
debug!(id = Device::get_id(self), "Updating state to {state}");
self.last_known_state = state;
// If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.config.timeout {
self.start_timeout(timeout).await.unwrap();
}
} else {
} else if self
.config
.remotes
.iter()
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
@ -166,7 +165,7 @@ impl OnPresence for IkeaOutlet {
async fn on_presence(&mut self, presence: bool) {
// Turn off the outlet when we leave the house (Not if it is a battery charger)
if !presence && self.config.outlet_type != OutletType::Charger {
debug!(id = self.identifier, "Turning device off");
debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok();
}
}
@ -186,7 +185,7 @@ impl GoogleHomeDevice for IkeaOutlet {
device::Name::new(&self.config.info.name)
}
fn get_id(&self) -> &str {
fn get_id(&self) -> String {
Device::get_id(self)
}
@ -228,13 +227,13 @@ impl crate::traits::Timeout for IkeaOutlet {
// get dropped
let client = self.config.client.clone();
let topic = self.config.mqtt.topic.clone();
let id = self.identifier.clone();
let id = Device::get_id(self).clone();
self.handle = Some(tokio::spawn(async move {
debug!(id, "Starting timeout ({timeout:?})...");
tokio::time::sleep(timeout).await;
debug!(id, "Turning outlet off!");
// TODO: Idealy we would call self.set_on(false), however since we want to do
// it after a timeout we have to put it in a seperate task.
// it after a timeout we have to put it in a separate task.
// I don't think we can really get around calling outside function
set_on(client, &topic, false).await;
}));

View File

@ -1,8 +1,9 @@
use std::net::SocketAddr;
use std::convert::Infallible;
use std::net::{Ipv4Addr, SocketAddr};
use std::str::Utf8Error;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError};
use google_home::traits;
@ -12,40 +13,34 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::trace;
use super::Device;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use super::{Device, LuaDeviceCreate};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct KasaOutletConfig {
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
addr: SocketAddr,
}
#[async_trait]
impl DeviceConfig for KasaOutletConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up KasaOutlet");
let device = KasaOutlet {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
pub identifier: String,
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 9999)))]
pub addr: SocketAddr,
}
#[derive(Debug, LuaDevice)]
pub struct KasaOutlet {
identifier: String,
#[config]
config: KasaOutletConfig,
}
#[async_trait]
impl LuaDeviceCreate for KasaOutlet {
type Config = KasaOutletConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up KasaOutlet");
Ok(Self { config })
}
}
impl Device for KasaOutlet {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}

View File

@ -1,65 +1,70 @@
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use rumqttc::Publish;
use tracing::{debug, trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::BrightnessMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct LightSensorConfig {
pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
#[device_config(rename("event_channel"), from(EventChannel), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
pub const DEFAULT: bool = false;
// TODO: The light sensor should get a list of devices that it should inform
#[async_trait]
impl DeviceConfig for LightSensorConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = LightSensor {
identifier: identifier.into(),
// Add helper type that does this conversion for us
config: self.clone(),
is_dark: DEFAULT,
};
Ok(Box::new(device))
}
}
const DEFAULT: bool = false;
#[derive(Debug, LuaDevice)]
pub struct LightSensor {
identifier: String,
#[config]
config: LightSensorConfig,
is_dark: bool,
}
#[async_trait]
impl LuaDeviceCreate for LightSensor {
type Config = LightSensorConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up LightSensor");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
is_dark: DEFAULT,
})
}
}
impl Device for LightSensor {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
#[async_trait]
impl OnMqtt for LightSensor {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let illuminance = match BrightnessMessage::try_from(message) {
Ok(state) => state.illuminance(),
Err(err) => {

View File

@ -3,15 +3,16 @@ mod audio_setup;
mod contact_sensor;
mod debug_bridge;
mod hue_bridge;
mod hue_light;
mod hue_group;
mod ikea_outlet;
mod kasa_outlet;
mod light_sensor;
mod ntfy;
pub mod ntfy;
mod presence;
mod wake_on_lan;
mod washer;
use async_trait::async_trait;
use google_home::device::AsGoogleHomeDevice;
use google_home::traits::OnOff;
@ -20,18 +21,46 @@ pub use self::audio_setup::*;
pub use self::contact_sensor::*;
pub use self::debug_bridge::*;
pub use self::hue_bridge::*;
pub use self::hue_light::*;
pub use self::hue_group::*;
pub use self::ikea_outlet::*;
pub use self::kasa_outlet::*;
pub use self::light_sensor::*;
pub use self::ntfy::{Notification, Ntfy};
pub use self::ntfy::{Ntfy, NtfyConfig};
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
pub use self::wake_on_lan::*;
pub use self::washer::*;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::traits::Timeout;
#[async_trait]
pub trait LuaDeviceCreate {
type Config;
type Error;
async fn create(config: Self::Config) -> Result<Self, Self::Error>
where
Self: Sized;
}
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
AirFilter::register_with_lua(lua)?;
AudioSetup::register_with_lua(lua)?;
ContactSensor::register_with_lua(lua)?;
DebugBridge::register_with_lua(lua)?;
HueBridge::register_with_lua(lua)?;
HueGroup::register_with_lua(lua)?;
IkeaOutlet::register_with_lua(lua)?;
KasaOutlet::register_with_lua(lua)?;
LightSensor::register_with_lua(lua)?;
Ntfy::register_with_lua(lua)?;
Presence::register_with_lua(lua)?;
WakeOnLAN::register_with_lua(lua)?;
Washer::register_with_lua(lua)?;
Ok(())
}
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
fn get_id(&self) -> &str;
fn get_id(&self) -> String;
}

View File

@ -1,21 +1,16 @@
use std::collections::HashMap;
use std::convert::Infallible;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use serde::Serialize;
use serde_repr::*;
use tracing::{debug, error, warn};
use tracing::{error, trace, warn};
use crate::config::NtfyConfig;
use super::LuaDeviceCreate;
use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
#[derive(Debug)]
pub struct Ntfy {
base_url: String,
topic: String,
tx: event::Sender,
}
#[derive(Debug, Serialize_repr, Clone, Copy)]
#[repr(u8)]
pub enum Priority {
@ -40,9 +35,9 @@ pub enum ActionType {
#[derive(Debug, Serialize, Clone)]
pub struct Action {
#[serde(flatten)]
action: ActionType,
label: String,
clear: Option<bool>,
pub action: ActionType,
pub label: String,
pub clear: Option<bool>,
}
#[derive(Serialize)]
@ -116,28 +111,50 @@ impl Default for Notification {
}
}
impl Ntfy {
pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self {
Self {
base_url: config.url,
topic: config.topic,
tx: event_channel.get_tx(),
}
}
#[derive(Debug, LuaDeviceConfig, LuaTypeDefinition)]
pub struct NtfyConfig {
#[device_config(default("https://ntfy.sh".into()))]
pub url: String,
pub topic: String,
#[device_config(rename("event_channel"), from_lua, from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
}
#[derive(Debug, LuaDevice)]
pub struct Ntfy {
config: NtfyConfig,
}
#[async_trait]
impl LuaDeviceCreate for Ntfy {
type Config = NtfyConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "ntfy", "Setting up Ntfy");
Ok(Self { config })
}
}
impl Device for Ntfy {
fn get_id(&self) -> String {
"ntfy".to_string()
}
}
impl Ntfy {
async fn send(&self, notification: Notification) {
let notification = notification.finalize(&self.topic);
debug!("Sending notfication");
let notification = notification.finalize(&self.config.topic);
// Create the request
let res = reqwest::Client::new()
.post(self.base_url.clone())
.post(self.config.url.clone())
.json(&notification)
.send()
.await;
if let Err(err) = res {
error!("Something went wrong while sending the notifcation: {err}");
error!("Something went wrong while sending the notification: {err}");
} else if let Ok(res) = res {
let status = res.status();
if !status.is_success() {
@ -147,12 +164,6 @@ impl Ntfy {
}
}
impl Device for Ntfy {
fn get_id(&self) -> &str {
"ntfy"
}
}
#[async_trait]
impl OnPresence for Ntfy {
async fn on_presence(&mut self, presence: bool) {
@ -177,7 +188,13 @@ impl OnPresence for Ntfy {
.add_action(action)
.set_priority(Priority::Low);
if self.tx.send(Event::Ntfy(notification)).await.is_err() {
if self
.config
.tx
.send(Event::Ntfy(notification))
.await
.is_err()
{
warn!("There are no receivers on the event channel");
}
}

View File

@ -1,60 +1,76 @@
use std::collections::HashMap;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, warn};
use tracing::{debug, trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig;
use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PresenceMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize)]
#[derive(Debug, LuaDeviceConfig, LuaTypeDefinition)]
pub struct PresenceConfig {
#[serde(flatten)]
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, rename("event_channel"), from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
pub const DEFAULT_PRESENCE: bool = false;
#[derive(Debug)]
#[derive(Debug, LuaDevice)]
pub struct Presence {
tx: event::Sender,
mqtt: MqttDeviceConfig,
config: PresenceConfig,
devices: HashMap<String, bool>,
current_overall_presence: bool,
}
impl Presence {
pub fn new(config: PresenceConfig, event_channel: &EventChannel) -> Self {
Self {
tx: event_channel.get_tx(),
mqtt: config.mqtt,
#[async_trait]
impl LuaDeviceCreate for Presence {
type Config = PresenceConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "ntfy", "Setting up Presence");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
devices: HashMap::new(),
current_overall_presence: DEFAULT_PRESENCE,
}
})
}
}
impl Device for Presence {
fn get_id(&self) -> &str {
"presence"
fn get_id(&self) -> String {
"presence".to_string()
}
}
#[async_trait]
impl OnMqtt for Presence {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let offset = self
.config
.mqtt
.topic
.find('+')
.or(self.mqtt.topic.find('#'))
.or(self.config.mqtt.topic.find('#'))
.expect("Presence::create fails if it does not contain wildcards");
let device_name = message.topic[offset..].into();
@ -81,6 +97,7 @@ impl OnMqtt for Presence {
self.current_overall_presence = overall_presence;
if self
.config
.tx
.send(Event::Presence(overall_presence))
.await

View File

@ -1,7 +1,7 @@
use std::net::Ipv4Addr;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use eui48::MacAddress;
use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene};
@ -10,69 +10,64 @@ use google_home::{device, GoogleHomeDevice};
use rumqttc::Publish;
use tracing::{debug, error, trace};
use super::Device;
use super::{Device, LuaDeviceCreate};
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::ActivateMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct WakeOnLANConfig {
#[device_config(flatten)]
info: InfoConfig,
pub info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
mac_address: MacAddress,
pub mqtt: MqttDeviceConfig,
pub mac_address: MacAddress,
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
broadcast_ip: Ipv4Addr,
}
#[async_trait]
impl DeviceConfig for WakeOnLANConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
room = self.info.room,
"Setting up WakeOnLAN"
);
debug!("broadcast_ip = {}", self.broadcast_ip);
let device = WakeOnLAN {
identifier: identifier.into(),
config: self.clone(),
};
Ok(Box::new(device))
}
pub broadcast_ip: Ipv4Addr,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct WakeOnLAN {
identifier: String,
#[config]
config: WakeOnLANConfig,
}
#[async_trait]
impl LuaDeviceCreate for WakeOnLAN {
type Config = WakeOnLANConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up WakeOnLAN");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self { config })
}
}
impl Device for WakeOnLAN {
fn get_id(&self) -> &str {
&self.identifier
fn get_id(&self) -> String {
self.config.info.identifier()
}
}
#[async_trait]
impl OnMqtt for WakeOnLAN {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let activate = match ActivateMessage::try_from(message) {
Ok(message) => message.activate(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
@ -93,7 +88,7 @@ impl GoogleHomeDevice for WakeOnLAN {
name
}
fn get_id(&self) -> &str {
fn get_id(&self) -> String {
Device::get_id(self)
}
@ -111,14 +106,14 @@ impl traits::Scene for WakeOnLAN {
async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> {
if activate {
debug!(
id = self.identifier,
id = Device::get_id(self),
"Activating Computer: {} (Sending to {})",
self.config.mac_address,
self.config.broadcast_ip
);
let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
|err| {
error!(id = self.identifier, "invalid mac address: {err}");
error!(id = Device::get_id(self), "invalid mac address: {err}");
google_home::errors::DeviceError::TransientError
},
)?;
@ -129,13 +124,16 @@ impl traits::Scene for WakeOnLAN {
)
.await
.map_err(|err| {
error!(id = self.identifier, "Failed to activate computer: {err}");
error!(
id = Device::get_id(self),
"Failed to activate computer: {err}"
);
google_home::errors::DeviceError::TransientError.into()
})
.map(|_| debug!(id = self.identifier, "Success!"))
.map(|_| debug!(id = Device::get_id(self), "Success!"))
} else {
debug!(
id = self.identifier,
id = Device::get_id(self),
"Trying to deactivate computer, this is not currently supported"
);
// We do not support deactivating this scene

View File

@ -1,72 +1,79 @@
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
use rumqttc::Publish;
use tracing::{debug, error, warn};
use tracing::{debug, error, trace, warn};
use super::ntfy::Priority;
use super::{Device, Notification};
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::devices::ntfy::Notification;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
pub struct WasherConfig {
pub identifier: String,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
pub mqtt: MqttDeviceConfig,
// Power in Watt
threshold: f32,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub threshold: f32,
#[device_config(rename("event_channel"), from_lua, from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
}
#[async_trait]
impl DeviceConfig for WasherConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = Washer {
identifier: identifier.into(),
config: self.clone(),
running: 0,
};
Ok(Box::new(device))
}
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
// TODO: Add google home integration
#[derive(Debug, LuaDevice)]
pub struct Washer {
identifier: String,
#[config]
config: WasherConfig,
running: isize,
}
impl Device for Washer {
fn get_id(&self) -> &str {
&self.identifier
#[async_trait]
impl LuaDeviceCreate for Washer {
type Config = WasherConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up Washer");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self { config, running: 0 })
}
}
// The washer needs to have a power draw above the theshold multiple times before the washer is
impl Device for Washer {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
// The washer needs to have a power draw above the threshold multiple times before the washer is
// actually marked as running
// This helps prevent false positives
const HYSTERESIS: isize = 10;
#[async_trait]
impl OnMqtt for Washer {
fn topics(&self) -> Vec<&str> {
vec![&self.config.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let power = match PowerMessage::try_from(message) {
Ok(state) => state.power(),
Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return;
}
};
@ -76,7 +83,7 @@ impl OnMqtt for Washer {
if power < self.config.threshold && self.running >= HYSTERESIS {
// The washer is done running
debug!(
id = self.identifier,
id = self.config.identifier,
power,
threshold = self.config.threshold,
"Washer is done"
@ -104,7 +111,7 @@ impl OnMqtt for Washer {
} else if power >= self.config.threshold && self.running < HYSTERESIS {
// Washer could be starting
debug!(
id = self.identifier,
id = self.config.identifier,
power,
threshold = self.config.threshold,
"Washer is starting"

View File

@ -65,16 +65,6 @@ pub enum ParseError {
InvalidPayload(Bytes),
}
#[derive(Debug, Error)]
pub enum ConfigParseError {
#[error(transparent)]
MissingEnv(#[from] MissingEnv),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
YamlError(#[from] serde_yaml::Error),
}
// TODO: Would be nice to somehow get the line number of the expected wildcard topic
#[derive(Debug, Error)]
#[error("Topic '{topic}' is expected to be a wildcard topic")]
@ -92,12 +82,10 @@ impl MissingWildcard {
#[derive(Debug, Error)]
pub enum DeviceConfigError {
#[error("Child '{1}' of device '{0}' does not exist")]
MissingChild(String, String),
#[error("Device '{0}' does not implement expected trait '{1}'")]
MissingTrait(String, String),
#[error(transparent)]
MissingWildcard(#[from] MissingWildcard),
MqttClientError(#[from] rumqttc::ClientError),
}
#[derive(Debug, Error)]

View File

@ -4,7 +4,7 @@ use mlua::FromLua;
use rumqttc::Publish;
use tokio::sync::mpsc;
use crate::devices::Notification;
use crate::devices::ntfy::Notification;
#[derive(Debug, Clone)]
pub enum Event {
@ -37,7 +37,7 @@ impl mlua::UserData for EventChannel {}
#[async_trait]
#[device_trait]
pub trait OnMqtt {
fn topics(&self) -> Vec<&str>;
// fn topics(&self) -> Vec<&str>;
async fn on_mqtt(&mut self, message: Publish);
}

View File

@ -1,6 +1,9 @@
#![allow(incomplete_features)]
#![feature(specialization)]
#![feature(let_chains)]
use once_cell::sync::Lazy;
use tokio::sync::Mutex;
pub mod auth;
pub mod config;
pub mod device_manager;
@ -11,3 +14,5 @@ pub mod messages;
pub mod mqtt;
pub mod schedule;
pub mod traits;
pub static LUA: Lazy<Mutex<mlua::Lua>> = Lazy::new(|| Mutex::new(mlua::Lua::new()));

View File

@ -1,15 +1,14 @@
#![feature(async_closure)]
use std::{fs, process};
use std::path::Path;
use std::process;
use automation::auth::{OpenIDConfig, User};
use automation::config::Config;
use anyhow::anyhow;
use automation::auth::User;
use automation::config::{FulfillmentConfig, MqttConfig};
use automation::device_manager::DeviceManager;
use automation::devices::{
AirFilter, AudioSetup, ContactSensor, DebugBridge, HueBridge, HueGroup, IkeaOutlet, KasaOutlet,
LightSensor, Ntfy, Presence, WakeOnLAN, Washer,
};
use automation::error::ApiError;
use automation::mqtt::{self, WrappedAsyncClient};
use automation::{devices, LUA};
use axum::extract::FromRef;
use axum::http::StatusCode;
use axum::response::IntoResponse;
@ -17,17 +16,18 @@ use axum::routing::post;
use axum::{Json, Router};
use dotenvy::dotenv;
use google_home::{GoogleHome, Request};
use mlua::LuaSerdeExt;
use rumqttc::AsyncClient;
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
#[derive(Clone)]
struct AppState {
pub openid: OpenIDConfig,
pub openid_url: String,
}
impl FromRef<AppState> for OpenIDConfig {
impl FromRef<AppState> for String {
fn from_ref(input: &AppState) -> Self {
input.openid.clone()
input.openid_url.clone()
}
}
@ -51,41 +51,32 @@ async fn app() -> anyhow::Result<()> {
info!("Starting automation_rs...");
let config_filename =
std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.yml".into());
let config = Config::parse_file(&config_filename)?;
// Create a mqtt client
// TODO: Since we wait with starting the eventloop we might fill the queue while setting up devices
let (client, eventloop) = AsyncClient::new(config.mqtt.clone(), 100);
// Setup the device handler
let device_manager = DeviceManager::new(client.clone());
let device_manager = DeviceManager::new().await;
device_manager.add_schedule(config.schedule).await;
let fulfillment_config = {
let lua = LUA.lock().await;
let event_channel = device_manager.event_channel();
lua.set_warning_function(|_lua, text, _cont| {
warn!("{text}");
Ok(())
});
// Create and add the presence system
{
let presence = Presence::new(config.presence, &event_channel);
device_manager.add(Box::new(presence)).await;
}
// Start the ntfy service if it is configured
if let Some(config) = config.ntfy {
let ntfy = Ntfy::new(config, &event_channel);
device_manager.add(Box::new(ntfy)).await;
}
// Lua testing
{
let lua = mlua::Lua::new();
let automation = lua.create_table()?;
let event_channel = device_manager.event_channel();
let new_mqtt_client = lua.create_function(move |lua, config: mlua::Value| {
let config: MqttConfig = lua.from_value(config)?;
// Create a mqtt client
// TODO: When starting up, the devices are not yet created, this could lead to a device being out of sync
let (client, eventloop) = AsyncClient::new(config.into(), 100);
mqtt::start(eventloop, &event_channel);
Ok(WrappedAsyncClient(client))
})?;
automation.set("new_mqtt_client", new_mqtt_client)?;
automation.set("device_manager", device_manager.clone())?;
automation.set("mqtt_client", WrappedAsyncClient(client.clone()))?;
automation.set("event_channel", device_manager.event_channel())?;
let util = lua.create_table()?;
let get_env = lua.create_function(|_lua, name: String| {
@ -96,37 +87,32 @@ async fn app() -> anyhow::Result<()> {
lua.globals().set("automation", automation)?;
// Register all the device types
AirFilter::register_with_lua(&lua)?;
AudioSetup::register_with_lua(&lua)?;
ContactSensor::register_with_lua(&lua)?;
DebugBridge::register_with_lua(&lua)?;
HueBridge::register_with_lua(&lua)?;
HueGroup::register_with_lua(&lua)?;
IkeaOutlet::register_with_lua(&lua)?;
KasaOutlet::register_with_lua(&lua)?;
LightSensor::register_with_lua(&lua)?;
WakeOnLAN::register_with_lua(&lua)?;
Washer::register_with_lua(&lua)?;
devices::register_with_lua(&lua)?;
// TODO: Make this not hardcoded
let filename = "config.lua";
let file = fs::read_to_string(filename)?;
match lua.load(file).set_name(filename).exec_async().await {
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,
}?;
}
// Wrap the mqtt eventloop and start listening for message
// NOTE: We wait until all the setup is done, as otherwise we might miss some messages
mqtt::start(eventloop, &event_channel);
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 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 fullfillment route
let fullfillment = Router::new().route(
// Create google home fulfillment route
let fulfillment = Router::new().route(
"/google_home",
post(async move |user: User, Json(payload): Json<Request>| {
debug!(username = user.preferred_username, "{payload:#?}");
@ -148,13 +134,13 @@ async fn app() -> anyhow::Result<()> {
// Combine together all the routes
let app = Router::new()
.nest("/fullfillment", fullfillment)
.nest("/fulfillment", fulfillment)
.with_state(AppState {
openid: config.openid,
openid_url: fulfillment_config.openid_url.clone(),
});
// Start the web server
let addr = config.fullfillment.into();
let addr = fulfillment_config.into();
info!("Server started on http://{addr}");
axum::Server::try_bind(&addr)?
.serve(app.into_make_service())