Compare commits
1 Commits
11d5d5db4d
...
c090b18f73
Author | SHA1 | Date | |
---|---|---|---|
c090b18f73 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
|
/definitions/generated
|
||||||
|
|
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -88,7 +88,6 @@ dependencies = [
|
||||||
"impl_cast",
|
"impl_cast",
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
"mlua",
|
"mlua",
|
||||||
"once_cell",
|
|
||||||
"paste",
|
"paste",
|
||||||
"pollster",
|
"pollster",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -1109,9 +1108,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
|
|
|
@ -42,8 +42,13 @@ enum_dispatch = "0.3.12"
|
||||||
indexmap = { version = "2.0.0", features = ["serde"] }
|
indexmap = { version = "2.0.0", features = ["serde"] }
|
||||||
serde_yaml = "0.9.27"
|
serde_yaml = "0.9.27"
|
||||||
tokio-cron-scheduler = "0.9.4"
|
tokio-cron-scheduler = "0.9.4"
|
||||||
mlua = { version = "0.9.7", features = ["lua54", "vendored", "macros", "serialize", "async", "send"] }
|
mlua = { version = "0.9.7", features = [
|
||||||
once_cell = "1.19.0"
|
"lua54",
|
||||||
|
"vendored",
|
||||||
|
"macros",
|
||||||
|
"serialize",
|
||||||
|
"async",
|
||||||
|
] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
|
|
||||||
ENV AUTOMATION_CONFIG=/app/config.lua
|
ENV AUTOMATION_CONFIG=/app/config.yml
|
||||||
COPY ./config.lua /app/config.lua
|
COPY ./config/config.yml /app/config.yml
|
||||||
|
|
||||||
COPY ./build/automation /app/automation
|
COPY ./build/automation /app/automation
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
mod lua_device;
|
mod lua_device;
|
||||||
mod lua_device_config;
|
mod lua_device_config;
|
||||||
mod lua_type_definition;
|
|
||||||
|
|
||||||
use lua_device::impl_lua_device_macro;
|
use lua_device::impl_lua_device_macro;
|
||||||
use lua_device_config::impl_lua_device_config_macro;
|
use lua_device_config::impl_lua_device_config_macro;
|
||||||
use lua_type_definition::impl_lua_type_definition;
|
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
#[proc_macro_derive(LuaDevice)]
|
#[proc_macro_derive(LuaDevice, attributes(config))]
|
||||||
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let ast = parse_macro_input!(input as DeriveInput);
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
@ -20,10 +18,3 @@ pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::T
|
||||||
|
|
||||||
impl_lua_device_config_macro(&ast).into()
|
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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,42 +1,63 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::DeriveInput;
|
use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed};
|
||||||
|
|
||||||
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
|
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
|
||||||
let name = &ast.ident;
|
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! {
|
let gen = quote! {
|
||||||
impl #name {
|
impl #name {
|
||||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||||
lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?)
|
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 {
|
impl mlua::UserData for #name {
|
||||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
methods.add_async_function("new", |lua, config: mlua::Value| async {
|
methods.add_function("new", |lua, config: mlua::Value| {
|
||||||
let config = mlua::FromLua::from_lua(config, lua)?;
|
let config: #config = mlua::FromLua::from_lua(config, lua)?;
|
||||||
|
let config: Box<dyn crate::device_manager::DeviceConfig> = Box::new(config);
|
||||||
// TODO: Using crate:: could cause issues
|
Ok(config)
|
||||||
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)))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let def = format!(
|
||||||
|
r#"--- @meta
|
||||||
|
--- @class {name}
|
||||||
|
{name} = {{}}
|
||||||
|
--- @param config {name}Config
|
||||||
|
--- @return Config
|
||||||
|
function {name}.new(config) end"#
|
||||||
|
);
|
||||||
|
|
||||||
|
File::create(format!("./definitions/generated/{name}.lua"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(def.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
gen
|
gen
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{quote, quote_spanned};
|
use quote::{quote, quote_spanned};
|
||||||
|
@ -23,7 +26,7 @@ mod kw {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Argument {
|
enum Argument {
|
||||||
Flatten {
|
Flatten {
|
||||||
_keyword: kw::flatten,
|
_keyword: kw::flatten,
|
||||||
},
|
},
|
||||||
|
@ -107,8 +110,8 @@ impl Parse for Argument {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Args {
|
struct Args {
|
||||||
pub(crate) args: Punctuated<Argument, Token![,]>,
|
args: Punctuated<Argument, Token![,]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for Args {
|
impl Parse for Args {
|
||||||
|
@ -218,21 +221,6 @@ fn field_from_lua(field: &Field) -> TokenStream {
|
||||||
temp.into()
|
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! {
|
Argument::With { expr, .. } => Some(quote! {
|
||||||
{
|
{
|
||||||
let temp = #value;
|
let temp = #value;
|
||||||
|
@ -247,7 +235,7 @@ fn field_from_lua(field: &Field) -> TokenStream {
|
||||||
[] => value,
|
[] => value,
|
||||||
[value] => value.to_owned(),
|
[value] => value.to_owned(),
|
||||||
_ => {
|
_ => {
|
||||||
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'with'")}
|
return quote_spanned! {field.span() => compile_error!("Only one of either 'from' or 'with' is allowed")}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -291,5 +279,15 @@ pub fn impl_lua_device_config_macro(ast: &DeriveInput) -> TokenStream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut def = format!("--- @meta\n--- @class {name}\n");
|
||||||
|
for field in fields {
|
||||||
|
def += &format!("--- @field {} any\n", field.ident.clone().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
File::create(format!("./definitions/generated/{name}.lua"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(def.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
impl_from_lua
|
impl_from_lua
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
280
config.lua
280
config.lua
|
@ -1,9 +1,5 @@
|
||||||
print("Hello from lua")
|
print("Hello from lua")
|
||||||
|
|
||||||
automation.fulfillment = {
|
|
||||||
openid_url = "https://login.huizinga.dev/api/oidc",
|
|
||||||
}
|
|
||||||
|
|
||||||
local debug, value = pcall(automation.util.get_env, "DEBUG")
|
local debug, value = pcall(automation.util.get_env, "DEBUG")
|
||||||
if debug and value ~= "true" then
|
if debug and value ~= "true" then
|
||||||
debug = false
|
debug = false
|
||||||
|
@ -17,157 +13,157 @@ local function mqtt_automation(topic)
|
||||||
return "automation/" .. topic
|
return "automation/" .. topic
|
||||||
end
|
end
|
||||||
|
|
||||||
local mqtt_client = automation.new_mqtt_client({
|
automation.device_manager:create(
|
||||||
host = debug and "olympus.lan.huizinga.dev" or "mosquitto",
|
"debug_bridge",
|
||||||
port = 8883,
|
DebugBridge.new({
|
||||||
client_name = debug and "automation-debug" or "automation_rs",
|
topic = mqtt_automation("debug"),
|
||||||
username = "mqtt",
|
client = automation.mqtt_client,
|
||||||
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_ip = "10.0.0.146"
|
||||||
local hue_token = automation.util.get_env("HUE_TOKEN")
|
local hue_token = automation.util.get_env("HUE_TOKEN")
|
||||||
|
|
||||||
automation.device_manager:add(HueBridge.new({
|
automation.device_manager:create(
|
||||||
identifier = "hue_bridge",
|
"hue_bridge",
|
||||||
ip = hue_ip,
|
HueBridge.new({
|
||||||
login = hue_token,
|
ip = hue_ip,
|
||||||
flags = {
|
login = hue_token,
|
||||||
presence = 41,
|
flags = {
|
||||||
darkness = 43,
|
presence = 41,
|
||||||
},
|
darkness = 43,
|
||||||
}))
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(LightSensor.new({
|
automation.device_manager:create(
|
||||||
identifier = "living_light_sensor",
|
"living_light_sensor",
|
||||||
topic = mqtt_z2m("living/light"),
|
LightSensor.new({
|
||||||
client = mqtt_client,
|
topic = mqtt_z2m("living/light"),
|
||||||
min = 22000,
|
min = 22000,
|
||||||
max = 23500,
|
max = 23500,
|
||||||
event_channel = automation.device_manager:event_channel(),
|
event_channel = automation.event_channel,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(WakeOnLAN.new({
|
automation.device_manager:create(
|
||||||
name = "Zeus",
|
"living_zeus",
|
||||||
room = "Living Room",
|
WakeOnLAN.new({
|
||||||
topic = mqtt_automation("appliance/living_room/zeus"),
|
name = "Zeus",
|
||||||
client = mqtt_client,
|
room = "Living Room",
|
||||||
mac_address = "30:9c:23:60:9c:13",
|
topic = mqtt_automation("appliance/living_room/zeus"),
|
||||||
broadcast_ip = "10.0.0.255",
|
mac_address = "30:9c:23:60:9c:13",
|
||||||
}))
|
broadcast_ip = "10.0.0.255",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
local living_mixer = KasaOutlet.new({ identifier = "living_mixer", ip = "10.0.0.49" })
|
local living_mixer = automation.device_manager:create("living_mixer", KasaOutlet.new({ ip = "10.0.0.49" }))
|
||||||
automation.device_manager:add(living_mixer)
|
local living_speakers = automation.device_manager:create("living_speakers", KasaOutlet.new({ ip = "10.0.0.182" }))
|
||||||
local living_speakers = KasaOutlet.new({ identifier = "living_speakers", ip = "10.0.0.182" })
|
|
||||||
automation.device_manager:add(living_speakers)
|
|
||||||
|
|
||||||
automation.device_manager:add(AudioSetup.new({
|
automation.device_manager:create(
|
||||||
identifier = "living_audio",
|
"living_audio",
|
||||||
topic = mqtt_z2m("living/remote"),
|
AudioSetup.new({
|
||||||
client = mqtt_client,
|
topic = mqtt_z2m("living/remote"),
|
||||||
mixer = living_mixer,
|
mixer = living_mixer,
|
||||||
speakers = living_speakers,
|
speakers = living_speakers,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(IkeaOutlet.new({
|
automation.device_manager:create(
|
||||||
outlet_type = "Kettle",
|
"kitchen_kettle",
|
||||||
name = "Kettle",
|
IkeaOutlet.new({
|
||||||
room = "Kitchen",
|
outlet_type = "Kettle",
|
||||||
topic = mqtt_z2m("kitchen/kettle"),
|
name = "Kettle",
|
||||||
client = mqtt_client,
|
room = "Kitchen",
|
||||||
timeout = debug and 5 or 300,
|
topic = mqtt_z2m("kitchen/kettle"),
|
||||||
remotes = {
|
client = automation.mqtt_client,
|
||||||
{ topic = mqtt_z2m("bedroom/remote") },
|
timeout = debug and 5 or 300,
|
||||||
{ topic = mqtt_z2m("kitchen/remote") },
|
remotes = {
|
||||||
},
|
{ topic = mqtt_z2m("bedroom/remote") },
|
||||||
}))
|
{ topic = mqtt_z2m("kitchen/remote") },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(IkeaOutlet.new({
|
automation.device_manager:create(
|
||||||
outlet_type = "Light",
|
"batchroom_light",
|
||||||
name = "Light",
|
IkeaOutlet.new({
|
||||||
room = "Bathroom",
|
outlet_type = "Light",
|
||||||
topic = mqtt_z2m("batchroom/light"),
|
name = "Light",
|
||||||
client = mqtt_client,
|
room = "Bathroom",
|
||||||
timeout = debug and 60 or 45 * 60,
|
topic = mqtt_z2m("batchroom/light"),
|
||||||
}))
|
client = automation.mqtt_client,
|
||||||
|
timeout = debug and 60 or 45 * 60,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(Washer.new({
|
automation.device_manager:create(
|
||||||
identifier = "bathroom_washer",
|
"bathroom_washer",
|
||||||
topic = mqtt_z2m("batchroom/washer"),
|
Washer.new({
|
||||||
client = mqtt_client,
|
topic = mqtt_z2m("batchroom/washer"),
|
||||||
threshold = 1,
|
threshold = 1,
|
||||||
event_channel = automation.device_manager:event_channel(),
|
event_channel = automation.event_channel,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(IkeaOutlet.new({
|
automation.device_manager:create(
|
||||||
outlet_type = "Charger",
|
"workbench_charger",
|
||||||
name = "Charger",
|
IkeaOutlet.new({
|
||||||
room = "Workbench",
|
outlet_type = "Charger",
|
||||||
topic = mqtt_z2m("workbench/charger"),
|
name = "Charger",
|
||||||
client = mqtt_client,
|
room = "Workbench",
|
||||||
timeout = debug and 5 or 20 * 3600,
|
topic = mqtt_z2m("workbench/charger"),
|
||||||
}))
|
client = automation.mqtt_client,
|
||||||
|
timeout = debug and 5 or 20 * 3600,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(IkeaOutlet.new({
|
automation.device_manager:create(
|
||||||
name = "Outlet",
|
"workbench_outlet",
|
||||||
room = "Workbench",
|
IkeaOutlet.new({
|
||||||
topic = mqtt_z2m("workbench/outlet"),
|
name = "Outlet",
|
||||||
client = mqtt_client,
|
room = "Workbench",
|
||||||
}))
|
topic = mqtt_z2m("workbench/outlet"),
|
||||||
|
client = automation.mqtt_client,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
local hallway_lights = automation.device_manager:add(HueGroup.new({
|
local hallway_lights = automation.device_manager:create(
|
||||||
identifier = "hallway_lights",
|
"hallway_lights",
|
||||||
ip = hue_ip,
|
HueGroup.new({
|
||||||
login = hue_token,
|
ip = hue_ip,
|
||||||
group_id = 81,
|
login = hue_token,
|
||||||
scene_id = "3qWKxGVadXFFG4o",
|
group_id = 81,
|
||||||
timer_id = 1,
|
scene_id = "3qWKxGVadXFFG4o",
|
||||||
remotes = {
|
timer_id = 1,
|
||||||
{ topic = mqtt_z2m("hallway/remote") },
|
remotes = {
|
||||||
},
|
{ topic = mqtt_z2m("hallway/remote") },
|
||||||
client = mqtt_client,
|
},
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
automation.device_manager:add(ContactSensor.new({
|
automation.device_manager:create(
|
||||||
identifier = "hallway_frontdoor",
|
"hallway_frontdoor",
|
||||||
topic = mqtt_z2m("hallway/frontdoor"),
|
ContactSensor.new({
|
||||||
client = mqtt_client,
|
topic = mqtt_z2m("hallway/frontdoor"),
|
||||||
presence = {
|
client = automation.mqtt_client,
|
||||||
topic = mqtt_automation("presence/contact/frontdoor"),
|
presence = {
|
||||||
timeout = debug and 10 or 15 * 60,
|
topic = mqtt_automation("presence/contact/frontdoor"),
|
||||||
},
|
timeout = debug and 10 or 15 * 60,
|
||||||
trigger = {
|
},
|
||||||
devices = { hallway_lights },
|
trigger = {
|
||||||
timeout = debug and 10 or 2 * 60,
|
devices = { hallway_lights },
|
||||||
},
|
timeout = debug and 10 or 2 * 60,
|
||||||
}))
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
local bedroom_air_filter = AirFilter.new({
|
automation.device_manager:create(
|
||||||
name = "Air Filter",
|
"bedroom_air_filter",
|
||||||
room = "Bedroom",
|
AirFilter.new({
|
||||||
topic = "pico/filter/bedroom",
|
name = "Air Filter",
|
||||||
client = mqtt_client,
|
room = "Bedroom",
|
||||||
})
|
topic = "pico/filter/bedroom",
|
||||||
automation.device_manager:add(bedroom_air_filter)
|
client = automation.mqtt_client,
|
||||||
|
})
|
||||||
automation.device_manager:schedule("0/1 * * * * *", function()
|
)
|
||||||
print("Device: " .. bedroom_air_filter:get_id())
|
|
||||||
end)
|
|
||||||
|
|
25
config/config.yml
Normal file
25
config/config.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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"
|
25
config/zeus.dev.yml
Normal file
25
config/zeus.dev.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
|
@ -1,40 +1,27 @@
|
||||||
--- @meta
|
--- @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 = {}
|
||||||
|
|
||||||
|
--- @class Device
|
||||||
|
--- @class Config
|
||||||
|
|
||||||
|
--- @class DeviceManager
|
||||||
automation.device_manager = {}
|
automation.device_manager = {}
|
||||||
--- @param device WrappedDevice
|
|
||||||
function automation.device_manager:add(device) end
|
|
||||||
|
|
||||||
--- @param when string
|
--- @class MqttClient
|
||||||
--- @param func function
|
automation.mqtt_client = {}
|
||||||
function automation.device_manager:schedule(when, func) end
|
|
||||||
|
|
||||||
automation.util = {}
|
--- @param identifier string
|
||||||
--- @param env string
|
--- @param config Config
|
||||||
--- @return string
|
--- @return Device
|
||||||
function automation.util.get_env(env) end
|
function automation.device_manager:create(identifier, config) end
|
||||||
|
|
||||||
--- @class Fulfillment
|
--- @class DebugBridge
|
||||||
--- @field openid_url string|nil
|
DebugBridge = {}
|
||||||
automation.fulfillment = {}
|
|
||||||
|
|
||||||
--- @class MqttConfig
|
--- @class DebugBridgeConfig
|
||||||
--- @param config MqttConfig
|
--- @field topic string
|
||||||
--- @return WrappedAsyncClient
|
|
||||||
function automation.new_mqtt_client(config) end
|
|
||||||
|
|
||||||
--- TODO: Generate this automatically
|
--- @param config DebugBridgeConfig
|
||||||
--- @alias OutletType "Outlet"|"Kettle"|"Charger"|"Light"
|
--- @return Config
|
||||||
--- @alias TriggerDevicesHelper WrappedDevice[]
|
function DebugBridge.new(config) end
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
-- 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
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
--- @meta
|
|
||||||
|
|
||||||
--- @alias String string
|
|
||||||
|
|
||||||
--- @alias u64 number
|
|
||||||
--- @alias isize number
|
|
||||||
--- @alias f32 number
|
|
||||||
|
|
||||||
--- @alias Ipv4Addr string
|
|
||||||
--- @alias MacAddress string
|
|
|
@ -46,10 +46,10 @@ where
|
||||||
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
||||||
fn get_device_type(&self) -> Type;
|
fn get_device_type(&self) -> Type;
|
||||||
fn get_device_name(&self) -> Name;
|
fn get_device_name(&self) -> Name;
|
||||||
fn get_id(&self) -> String;
|
fn get_id(&self) -> &str;
|
||||||
fn is_online(&self) -> bool;
|
fn is_online(&self) -> bool;
|
||||||
|
|
||||||
// Default values that can optionally be overridden
|
// Default values that can optionally be overriden
|
||||||
fn will_report_state(&self) -> bool {
|
fn will_report_state(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
||||||
async fn sync(&self) -> response::sync::Device {
|
async fn sync(&self) -> response::sync::Device {
|
||||||
let name = self.get_device_name();
|
let name = self.get_device_name();
|
||||||
let mut device =
|
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.name = name;
|
||||||
device.will_report_state = self.will_report_state();
|
device.will_report_state = self.will_report_state();
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub struct GoogleHome {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FulfillmentError {
|
pub enum FullfillmentError {
|
||||||
#[error("Expected at least one ResponsePayload")]
|
#[error("Expected at least one ResponsePayload")]
|
||||||
ExpectedOnePayload,
|
ExpectedOnePayload,
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ impl GoogleHome {
|
||||||
&self,
|
&self,
|
||||||
request: Request,
|
request: Request,
|
||||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||||
) -> Result<Response, FulfillmentError> {
|
) -> Result<Response, FullfillmentError> {
|
||||||
// TODO: What do we do if we actually get more then one thing in the input array, right now
|
// 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
|
// we only respond to the first thing
|
||||||
let intent = request.inputs.into_iter().next();
|
let intent = request.inputs.into_iter().next();
|
||||||
|
@ -54,7 +54,7 @@ impl GoogleHome {
|
||||||
|
|
||||||
payload
|
payload
|
||||||
.await
|
.await
|
||||||
.ok_or(FulfillmentError::ExpectedOnePayload)
|
.ok_or(FullfillmentError::ExpectedOnePayload)
|
||||||
.map(|payload| Response::new(&request.request_id, payload))
|
.map(|payload| Response::new(&request.request_id, payload))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
#![feature(specialization)]
|
#![feature(specialization)]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
pub mod device;
|
pub mod device;
|
||||||
mod fulfillment;
|
mod fullfillment;
|
||||||
|
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
@ -13,6 +13,6 @@ pub mod traits;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use device::GoogleHomeDevice;
|
pub use device::GoogleHomeDevice;
|
||||||
pub use fulfillment::{FulfillmentError, GoogleHome};
|
pub use fullfillment::{FullfillmentError, GoogleHome};
|
||||||
pub use request::Request;
|
pub use request::Request;
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
|
|
11
src/auth.rs
11
src/auth.rs
|
@ -6,6 +6,11 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use crate::error::{ApiError, ApiErrorJson};
|
use crate::error::{ApiError, ApiErrorJson};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenIDConfig {
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub preferred_username: String,
|
pub preferred_username: String,
|
||||||
|
@ -14,18 +19,18 @@ pub struct User {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S> FromRequestParts<S> for User
|
impl<S> FromRequestParts<S> for User
|
||||||
where
|
where
|
||||||
String: FromRef<S>,
|
OpenIDConfig: FromRef<S>,
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
{
|
{
|
||||||
type Rejection = ApiError;
|
type Rejection = ApiError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
// Get the state
|
// Get the state
|
||||||
let openid_url = String::from_ref(state);
|
let openid = OpenIDConfig::from_ref(state);
|
||||||
|
|
||||||
// Create a request to the auth server
|
// Create a request to the auth server
|
||||||
// TODO: Do some discovery to find the correct url for this instead of assuming
|
// TODO: Do some discovery to find the correct url for this instead of assuming
|
||||||
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url));
|
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid.base_url));
|
||||||
|
|
||||||
// Add auth header to the request if it exists
|
// Add auth header to the request if it exists
|
||||||
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {
|
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
109
src/config.rs
109
src/config.rs
|
@ -1,9 +1,28 @@
|
||||||
|
use std::fs;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use automation_macro::LuaTypeDefinition;
|
use regex::{Captures, Regex};
|
||||||
use rumqttc::{MqttOptions, Transport};
|
use rumqttc::{MqttOptions, Transport};
|
||||||
use serde::Deserialize;
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct MqttConfig {
|
pub struct MqttConfig {
|
||||||
|
@ -30,46 +49,90 @@ 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)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct FulfillmentConfig {
|
pub struct FullfillmentConfig {
|
||||||
pub openid_url: String,
|
#[serde(default = "default_fullfillment_ip")]
|
||||||
#[serde(default = "default_fulfillment_ip")]
|
|
||||||
pub ip: Ipv4Addr,
|
pub ip: Ipv4Addr,
|
||||||
#[serde(default = "default_fulfillment_port")]
|
#[serde(default = "default_fullfillment_port")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FulfillmentConfig> for SocketAddr {
|
impl From<FullfillmentConfig> for SocketAddr {
|
||||||
fn from(fulfillment: FulfillmentConfig) -> Self {
|
fn from(fullfillment: FullfillmentConfig) -> Self {
|
||||||
(fulfillment.ip, fulfillment.port).into()
|
(fullfillment.ip, fullfillment.port).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_fulfillment_ip() -> Ipv4Addr {
|
impl Default for FullfillmentConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ip: default_fullfillment_ip(),
|
||||||
|
port: default_fullfillment_port(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_fullfillment_ip() -> Ipv4Addr {
|
||||||
[0, 0, 0, 0].into()
|
[0, 0, 0, 0].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_fulfillment_port() -> u16 {
|
fn default_fullfillment_port() -> u16 {
|
||||||
7878
|
7878
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
|
#[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)]
|
||||||
pub struct InfoConfig {
|
pub struct InfoConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub room: Option<String>,
|
pub room: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfoConfig {
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
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 struct MqttDeviceConfig {
|
||||||
pub topic: String,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,21 +2,33 @@ use std::collections::HashMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use enum_dispatch::enum_dispatch;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use google_home::traits::OnOff;
|
||||||
use mlua::FromLua;
|
use mlua::FromLua;
|
||||||
|
use rumqttc::{matches, AsyncClient, QoS};
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||||
use tracing::{debug, instrument, trace};
|
use tracing::{debug, error, instrument, trace};
|
||||||
|
|
||||||
use crate::devices::{As, Device};
|
use crate::devices::{As, Device};
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
|
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||||
use crate::LUA;
|
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> {}
|
||||||
|
|
||||||
#[derive(Debug, FromLua, Clone)]
|
#[derive(Debug, FromLua, Clone)]
|
||||||
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
|
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
|
||||||
|
|
||||||
impl WrappedDevice {
|
impl WrappedDevice {
|
||||||
pub fn new(device: Box<dyn Device>) -> Self {
|
fn new(device: Box<dyn Device>) -> Self {
|
||||||
Self(Arc::new(RwLock::new(device)))
|
Self(Arc::new(RwLock::new(device)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,31 +46,25 @@ impl DerefMut for WrappedDevice {
|
||||||
&mut self.0
|
&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>>>>;
|
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DeviceManager {
|
pub struct DeviceManager {
|
||||||
devices: Arc<RwLock<DeviceMap>>,
|
devices: Arc<RwLock<DeviceMap>>,
|
||||||
|
client: AsyncClient,
|
||||||
event_channel: EventChannel,
|
event_channel: EventChannel,
|
||||||
scheduler: JobScheduler,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceManager {
|
impl DeviceManager {
|
||||||
pub async fn new() -> Self {
|
pub fn new(client: AsyncClient) -> Self {
|
||||||
let (event_channel, mut event_rx) = EventChannel::new();
|
let (event_channel, mut event_rx) = EventChannel::new();
|
||||||
|
|
||||||
let device_manager = Self {
|
let device_manager = Self {
|
||||||
devices: Arc::new(RwLock::new(HashMap::new())),
|
devices: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
client,
|
||||||
event_channel,
|
event_channel,
|
||||||
scheduler: JobScheduler::new().await.unwrap(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn({
|
tokio::spawn({
|
||||||
|
@ -74,17 +80,81 @@ impl DeviceManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
device_manager.scheduler.start().await.unwrap();
|
|
||||||
|
|
||||||
device_manager
|
device_manager
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add(&self, device: &WrappedDevice) {
|
// TODO: This function is currently extremely cursed...
|
||||||
let id = device.read().await.get_id();
|
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();
|
||||||
|
|
||||||
debug!(id, "Adding device");
|
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());
|
self.devices.write().await.insert(id, device.0.clone());
|
||||||
|
|
||||||
|
device
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event_channel(&self) -> EventChannel {
|
pub fn event_channel(&self) -> EventChannel {
|
||||||
|
@ -115,16 +185,15 @@ impl DeviceManager {
|
||||||
let mut device = device.write().await;
|
let mut device = device.write().await;
|
||||||
let device = device.as_mut();
|
let device = device.as_mut();
|
||||||
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
|
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
|
||||||
// let subscribed = device
|
let subscribed = device
|
||||||
// .topics()
|
.topics()
|
||||||
// .iter()
|
.iter()
|
||||||
// .any(|topic| matches(&message.topic, topic));
|
.any(|topic| matches(&message.topic, topic));
|
||||||
//
|
|
||||||
// if subscribed {
|
if subscribed {
|
||||||
trace!(id, "Handling");
|
trace!(id, "Handling");
|
||||||
device.on_mqtt(message).await;
|
device.on_mqtt(message).await;
|
||||||
trace!(id, "Done");
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -139,7 +208,6 @@ impl DeviceManager {
|
||||||
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
|
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
|
||||||
trace!(id, "Handling");
|
trace!(id, "Handling");
|
||||||
device.on_darkness(dark).await;
|
device.on_darkness(dark).await;
|
||||||
trace!(id, "Done");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -153,7 +221,6 @@ impl DeviceManager {
|
||||||
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
|
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
|
||||||
trace!(id, "Handling");
|
trace!(id, "Handling");
|
||||||
device.on_presence(presence).await;
|
device.on_presence(presence).await;
|
||||||
trace!(id, "Done");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -169,7 +236,6 @@ impl DeviceManager {
|
||||||
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
|
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
|
||||||
trace!(id, "Handling");
|
trace!(id, "Handling");
|
||||||
device.on_notification(notification).await;
|
device.on_notification(notification).await;
|
||||||
trace!(id, "Done");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -182,47 +248,19 @@ impl DeviceManager {
|
||||||
|
|
||||||
impl mlua::UserData for DeviceManager {
|
impl mlua::UserData for DeviceManager {
|
||||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
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(
|
methods.add_async_method(
|
||||||
"schedule",
|
"create",
|
||||||
|lua, this, (schedule, f): (String, mlua::Function)| async move {
|
|_lua, this, (identifier, config): (String, mlua::Value)| async move {
|
||||||
debug!("schedule = {schedule}");
|
// TODO: Handle the error here properly
|
||||||
let uuid = this
|
let config: Box<dyn DeviceConfig> = config.as_userdata().unwrap().take()?;
|
||||||
.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();
|
|
||||||
|
|
||||||
f.call::<_, ()>(()).unwrap();
|
let device = config
|
||||||
})
|
.create(&identifier)
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.map_err(mlua::ExternalError::into_lua_err)?;
|
||||||
|
|
||||||
// Store the function in the registry
|
Ok(this.add(device).await)
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,51 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use google_home::device::Name;
|
use google_home::device::Name;
|
||||||
use google_home::errors::ErrorCode;
|
use google_home::errors::ErrorCode;
|
||||||
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
|
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
|
||||||
use google_home::types::Type;
|
use google_home::types::Type;
|
||||||
use google_home::GoogleHomeDevice;
|
use google_home::GoogleHomeDevice;
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
|
||||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::OnMqtt;
|
use crate::event::OnMqtt;
|
||||||
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
|
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
use crate::mqtt::WrappedAsyncClient;
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct AirFilterConfig {
|
pub struct AirFilterConfig {
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub info: InfoConfig,
|
info: InfoConfig,
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub client: WrappedAsyncClient,
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct AirFilter {
|
pub struct AirFilter {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: AirFilterConfig,
|
config: AirFilterConfig,
|
||||||
|
|
||||||
last_known_state: AirFilterState,
|
last_known_state: AirFilterState,
|
||||||
|
@ -41,7 +60,7 @@ impl AirFilter {
|
||||||
self.config
|
self.config
|
||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
topic.clone(),
|
||||||
rumqttc::QoS::AtLeastOnce,
|
rumqttc::QoS::AtLeastOnce,
|
||||||
false,
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
|
@ -52,46 +71,23 @@ 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 {
|
impl Device for AirFilter {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.info.identifier()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for AirFilter {
|
impl OnMqtt for AirFilter {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let state = match AirFilterState::try_from(message) {
|
let state = match AirFilterState::try_from(message) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -100,7 +96,7 @@ impl OnMqtt for AirFilter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(id = Device::get_id(self), "Updating state to {state:?}");
|
debug!(id = self.identifier, "Updating state to {state:?}");
|
||||||
|
|
||||||
self.last_known_state = state;
|
self.last_known_state = state;
|
||||||
}
|
}
|
||||||
|
@ -115,7 +111,7 @@ impl GoogleHomeDevice for AirFilter {
|
||||||
Name::new(&self.config.info.name)
|
Name::new(&self.config.info.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
Device::get_id(self)
|
Device::get_id(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,82 +1,75 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::Device;
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
use crate::device_manager::WrappedDevice;
|
use crate::device_manager::{DeviceConfig, WrappedDevice};
|
||||||
use crate::devices::As;
|
use crate::devices::As;
|
||||||
use crate::error::DeviceConfigError;
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{OnMqtt, OnPresence};
|
use crate::event::{OnMqtt, OnPresence};
|
||||||
use crate::messages::{RemoteAction, RemoteMessage};
|
use crate::messages::{RemoteAction, RemoteMessage};
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct AudioSetupConfig {
|
pub struct AudioSetupConfig {
|
||||||
pub identifier: String,
|
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub mixer: WrappedDevice,
|
mixer: WrappedDevice,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub speakers: WrappedDevice,
|
speakers: WrappedDevice,
|
||||||
#[device_config(from_lua)]
|
|
||||||
pub client: WrappedAsyncClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
|
||||||
pub struct AudioSetup {
|
|
||||||
config: AudioSetupConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LuaDeviceCreate for AudioSetup {
|
impl DeviceConfig for AudioSetupConfig {
|
||||||
type Config = AudioSetupConfig;
|
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
|
||||||
type Error = DeviceConfigError;
|
trace!(id = identifier, "Setting up AudioSetup");
|
||||||
|
|
||||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
let mixer_id = self.mixer.read().await.get_id().to_owned();
|
||||||
trace!(id = config.identifier, "Setting up AudioSetup");
|
if !As::<dyn OnOff>::is(self.mixer.read().await.as_ref()) {
|
||||||
|
|
||||||
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()));
|
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let speakers_id = config.speakers.read().await.get_id().to_owned();
|
let speakers_id = self.speakers.read().await.get_id().to_owned();
|
||||||
if !As::<dyn OnOff>::is(config.speakers.read().await.as_ref()) {
|
if !As::<dyn OnOff>::is(self.speakers.read().await.as_ref()) {
|
||||||
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
|
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
let device = AudioSetup {
|
||||||
.client
|
identifier: identifier.into(),
|
||||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
config: self.clone(),
|
||||||
.await?;
|
};
|
||||||
|
|
||||||
Ok(AudioSetup { config })
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
impl Device for AudioSetup {
|
impl Device for AudioSetup {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for AudioSetup {
|
impl OnMqtt for AudioSetup {
|
||||||
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
||||||
let action = match RemoteMessage::try_from(message) {
|
let action = match RemoteMessage::try_from(message) {
|
||||||
Ok(message) => message.action(),
|
Ok(message) => message.action(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Failed to parse message: {err}"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -125,7 +118,7 @@ impl OnPresence for AudioSetup {
|
||||||
) {
|
) {
|
||||||
// Turn off the audio setup when we leave the house
|
// Turn off the audio setup when we leave the house
|
||||||
if !presence {
|
if !presence {
|
||||||
debug!(id = self.config.identifier, "Turning devices off");
|
debug!(id = self.identifier, "Turning devices off");
|
||||||
speakers.set_on(false).await.unwrap();
|
speakers.set_on(false).await.unwrap();
|
||||||
mixer.set_on(false).await.unwrap();
|
mixer.set_on(false).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
use mlua::FromLua;
|
use mlua::FromLua;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::Device;
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
use crate::device_manager::WrappedDevice;
|
use crate::device_manager::{DeviceConfig, WrappedDevice};
|
||||||
use crate::devices::{As, DEFAULT_PRESENCE};
|
use crate::devices::{As, DEFAULT_PRESENCE};
|
||||||
use crate::error::DeviceConfigError;
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{OnMqtt, OnPresence};
|
use crate::event::{OnMqtt, OnPresence};
|
||||||
|
@ -18,11 +18,11 @@ use crate::mqtt::WrappedAsyncClient;
|
||||||
use crate::traits::Timeout;
|
use crate::traits::Timeout;
|
||||||
|
|
||||||
// NOTE: If we add more presence devices we might need to move this out of here
|
// NOTE: If we add more presence devices we might need to move this out of here
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct PresenceDeviceConfig {
|
pub struct PresenceDeviceConfig {
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
pub mqtt: MqttDeviceConfig,
|
||||||
#[device_config(from(u64), with(Duration::from_secs))]
|
#[device_config(with(Duration::from_secs))]
|
||||||
pub timeout: Duration,
|
pub timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,46 +41,33 @@ impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct TriggerConfig {
|
pub struct TriggerConfig {
|
||||||
#[device_config(from_lua, from(TriggerDevicesHelper))]
|
#[device_config(from_lua, from(TriggerDevicesHelper))]
|
||||||
pub devices: Vec<(WrappedDevice, bool)>,
|
devices: Vec<(WrappedDevice, bool)>,
|
||||||
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
|
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||||
pub timeout: Option<Duration>,
|
pub timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct ContactSensorConfig {
|
pub struct ContactSensorConfig {
|
||||||
pub identifier: String,
|
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub presence: Option<PresenceDeviceConfig>,
|
presence: Option<PresenceDeviceConfig>,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub trigger: Option<TriggerConfig>,
|
trigger: Option<TriggerConfig>,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub client: WrappedAsyncClient,
|
client: WrappedAsyncClient,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
|
||||||
pub struct ContactSensor {
|
|
||||||
config: ContactSensorConfig,
|
|
||||||
|
|
||||||
overall_presence: bool,
|
|
||||||
is_closed: bool,
|
|
||||||
handle: Option<JoinHandle<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LuaDeviceCreate for ContactSensor {
|
impl DeviceConfig for ContactSensorConfig {
|
||||||
type Config = ContactSensorConfig;
|
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
|
||||||
type Error = DeviceConfigError;
|
trace!(id = identifier, "Setting up ContactSensor");
|
||||||
|
|
||||||
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
|
// Make sure the devices implement the required traits
|
||||||
if let Some(trigger) = &config.trigger {
|
if let Some(trigger) = &self.trigger {
|
||||||
for (device, _) in &trigger.devices {
|
for (device, _) in &trigger.devices {
|
||||||
let id = device.read().await.get_id().to_owned();
|
let id = device.read().await.get_id().to_owned();
|
||||||
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
|
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
|
||||||
|
@ -94,23 +81,32 @@ impl LuaDeviceCreate for ContactSensor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
let device = ContactSensor {
|
||||||
.client
|
identifier: identifier.into(),
|
||||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
config: self.clone(),
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config: config.clone(),
|
|
||||||
overall_presence: DEFAULT_PRESENCE,
|
overall_presence: DEFAULT_PRESENCE,
|
||||||
is_closed: true,
|
is_closed: true,
|
||||||
handle: None,
|
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 {
|
impl Device for ContactSensor {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,18 +119,15 @@ impl OnPresence for ContactSensor {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for ContactSensor {
|
impl OnMqtt for ContactSensor {
|
||||||
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
||||||
let is_closed = match ContactMessage::try_from(message) {
|
let is_closed = match ContactMessage::try_from(message) {
|
||||||
Ok(state) => state.is_closed(),
|
Ok(state) => state.is_closed(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Failed to parse message: {err}"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -143,7 +136,7 @@ impl OnMqtt for ContactSensor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(id = self.config.identifier, "Updating state to {is_closed}");
|
debug!(id = self.identifier, "Updating state to {is_closed}");
|
||||||
self.is_closed = is_closed;
|
self.is_closed = is_closed;
|
||||||
|
|
||||||
if let Some(trigger) = &mut self.config.trigger {
|
if let Some(trigger) = &mut self.config.trigger {
|
||||||
|
@ -195,7 +188,7 @@ impl OnMqtt for ContactSensor {
|
||||||
self.config
|
self.config
|
||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
&presence.mqtt.topic,
|
presence.mqtt.topic.clone(),
|
||||||
rumqttc::QoS::AtLeastOnce,
|
rumqttc::QoS::AtLeastOnce,
|
||||||
false,
|
false,
|
||||||
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
|
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
|
||||||
|
@ -212,7 +205,7 @@ impl OnMqtt for ContactSensor {
|
||||||
} else {
|
} else {
|
||||||
// Once the door is closed again we start a timeout for removing the presence
|
// Once the door is closed again we start a timeout for removing the presence
|
||||||
let client = self.config.client.clone();
|
let client = self.config.client.clone();
|
||||||
let id = self.config.identifier.clone();
|
let id = self.identifier.clone();
|
||||||
let timeout = presence.timeout;
|
let timeout = presence.timeout;
|
||||||
let topic = presence.mqtt.topic.clone();
|
let topic = presence.mqtt.topic.clone();
|
||||||
self.handle = Some(tokio::spawn(async move {
|
self.handle = Some(tokio::spawn(async move {
|
||||||
|
@ -220,7 +213,7 @@ impl OnMqtt for ContactSensor {
|
||||||
tokio::time::sleep(timeout).await;
|
tokio::time::sleep(timeout).await;
|
||||||
debug!(id, "Removing door device!");
|
debug!(id, "Removing door device!");
|
||||||
client
|
client
|
||||||
.publish(&topic, rumqttc::QoS::AtLeastOnce, false, "")
|
.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "")
|
||||||
.await
|
.await
|
||||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
@ -1,44 +1,45 @@
|
||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use tracing::{trace, warn};
|
use tracing::warn;
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{OnDarkness, OnPresence};
|
use crate::event::{OnDarkness, OnPresence};
|
||||||
use crate::messages::{DarknessMessage, PresenceMessage};
|
use crate::messages::{DarknessMessage, PresenceMessage};
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
use crate::mqtt::WrappedAsyncClient;
|
||||||
|
|
||||||
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
|
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||||
pub struct DebugBridgeConfig {
|
pub struct DebugBridgeConfig {
|
||||||
pub identifier: String,
|
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
pub mqtt: MqttDeviceConfig,
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub client: WrappedAsyncClient,
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct DebugBridge {
|
pub struct DebugBridge {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: DebugBridgeConfig,
|
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 {
|
impl Device for DebugBridge {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::convert::Infallible;
|
use std::net::SocketAddr;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{error, trace, warn};
|
use tracing::{error, trace, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
use crate::device_manager::DeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{OnDarkness, OnPresence};
|
use crate::event::{OnDarkness, OnPresence};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -16,23 +16,36 @@ pub enum Flag {
|
||||||
Darkness,
|
Darkness,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct FlagIDs {
|
pub struct FlagIDs {
|
||||||
presence: isize,
|
pub presence: isize,
|
||||||
darkness: isize,
|
pub darkness: isize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
|
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||||
pub struct HueBridgeConfig {
|
pub struct HueBridgeConfig {
|
||||||
pub identifier: String,
|
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
|
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
pub login: String,
|
pub login: String,
|
||||||
pub flags: FlagIDs,
|
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)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct HueBridge {
|
pub struct HueBridge {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: HueBridgeConfig,
|
config: HueBridgeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,17 +54,6 @@ struct FlagMessage {
|
||||||
flag: bool,
|
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 {
|
impl HueBridge {
|
||||||
pub async fn set_flag(&self, flag: Flag, value: bool) {
|
pub async fn set_flag(&self, flag: Flag, value: bool) {
|
||||||
let flag_id = match flag {
|
let flag_id = match flag {
|
||||||
|
@ -86,8 +88,8 @@ impl HueBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device for HueBridge {
|
impl Device for HueBridge {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::SocketAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use google_home::errors::ErrorCode;
|
use google_home::errors::ErrorCode;
|
||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
use rumqttc::{Publish, SubscribeFilter};
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::Device;
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::OnMqtt;
|
use crate::event::OnMqtt;
|
||||||
use crate::messages::{RemoteAction, RemoteMessage};
|
use crate::messages::{RemoteAction, RemoteMessage};
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
use crate::traits::Timeout;
|
use crate::traits::Timeout;
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct HueGroupConfig {
|
pub struct HueGroupConfig {
|
||||||
pub identifier: String,
|
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
|
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
pub login: String,
|
pub login: String,
|
||||||
pub group_id: isize,
|
pub group_id: isize,
|
||||||
|
@ -27,38 +27,28 @@ pub struct HueGroupConfig {
|
||||||
pub scene_id: String,
|
pub scene_id: String,
|
||||||
#[device_config(default)]
|
#[device_config(default)]
|
||||||
pub remotes: Vec<MqttDeviceConfig>,
|
pub remotes: Vec<MqttDeviceConfig>,
|
||||||
#[device_config(from_lua)]
|
}
|
||||||
pub client: WrappedAsyncClient,
|
|
||||||
|
#[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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct HueGroup {
|
pub struct HueGroup {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: HueGroupConfig,
|
config: HueGroupConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Couple of helper function to get the correct urls
|
// 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 {
|
impl HueGroup {
|
||||||
fn url_base(&self) -> String {
|
fn url_base(&self) -> String {
|
||||||
format!("http://{}/api/{}", self.config.addr, self.config.login)
|
format!("http://{}/api/{}", self.config.addr, self.config.login)
|
||||||
|
@ -78,30 +68,26 @@ impl HueGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device for HueGroup {
|
impl Device for HueGroup {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for HueGroup {
|
impl OnMqtt for HueGroup {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !self
|
self.config
|
||||||
.config
|
|
||||||
.remotes
|
.remotes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
|
.map(|mqtt| mqtt.topic.as_str())
|
||||||
{
|
.collect()
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let action = match RemoteMessage::try_from(message) {
|
let action = match RemoteMessage::try_from(message) {
|
||||||
Ok(message) => message.action(),
|
Ok(message) => message.action(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Failed to parse message: {err}"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -140,13 +126,10 @@ impl OnOff for HueGroup {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
warn!(
|
warn!(id = self.identifier, "Status code is not success: {status}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Status code is not success: {status}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
|
Err(err) => error!(id = self.identifier, "Error: {err}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -162,19 +145,13 @@ impl OnOff for HueGroup {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
warn!(
|
warn!(id = self.identifier, "Status code is not success: {status}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Status code is not success: {status}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let on = match res.json::<message::Info>().await {
|
let on = match res.json::<message::Info>().await {
|
||||||
Ok(info) => info.any_on(),
|
Ok(info) => info.any_on(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Failed to parse message: {err}"
|
|
||||||
);
|
|
||||||
// TODO: Error code
|
// TODO: Error code
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
@ -182,7 +159,7 @@ impl OnOff for HueGroup {
|
||||||
|
|
||||||
return Ok(on);
|
return Ok(on);
|
||||||
}
|
}
|
||||||
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
|
Err(err) => error!(id = self.identifier, "Error: {err}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
|
@ -2,19 +2,20 @@ use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use google_home::errors::ErrorCode;
|
use google_home::errors::ErrorCode;
|
||||||
use google_home::traits::{self, OnOff};
|
use google_home::traits::{self, OnOff};
|
||||||
use google_home::types::Type;
|
use google_home::types::Type;
|
||||||
use google_home::{device, GoogleHomeDevice};
|
use google_home::{device, GoogleHomeDevice};
|
||||||
use rumqttc::{matches, Publish, SubscribeFilter};
|
use rumqttc::{matches, Publish};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
|
||||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{OnMqtt, OnPresence};
|
use crate::event::{OnMqtt, OnPresence};
|
||||||
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
|
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
use crate::mqtt::WrappedAsyncClient;
|
||||||
|
@ -28,25 +29,48 @@ pub enum OutletType {
|
||||||
Light,
|
Light,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct IkeaOutletConfig {
|
pub struct IkeaOutletConfig {
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub info: InfoConfig,
|
info: InfoConfig,
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
#[device_config(default(OutletType::Outlet))]
|
#[device_config(default(OutletType::Outlet))]
|
||||||
pub outlet_type: OutletType,
|
outlet_type: OutletType,
|
||||||
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
|
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||||
pub timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
#[device_config(default)]
|
#[device_config(default)]
|
||||||
pub remotes: Vec<MqttDeviceConfig>,
|
pub remotes: Vec<MqttDeviceConfig>,
|
||||||
|
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub client: WrappedAsyncClient,
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct IkeaOutlet {
|
pub struct IkeaOutlet {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: IkeaOutletConfig,
|
config: IkeaOutletConfig,
|
||||||
|
|
||||||
last_known_state: bool,
|
last_known_state: bool,
|
||||||
|
@ -60,7 +84,7 @@ async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
|
||||||
// TODO: Handle potential errors here
|
// TODO: Handle potential errors here
|
||||||
client
|
client
|
||||||
.publish(
|
.publish(
|
||||||
&topic,
|
topic.clone(),
|
||||||
rumqttc::QoS::AtLeastOnce,
|
rumqttc::QoS::AtLeastOnce,
|
||||||
false,
|
false,
|
||||||
serde_json::to_string(&message).unwrap(),
|
serde_json::to_string(&message).unwrap(),
|
||||||
|
@ -70,45 +94,27 @@ async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
|
||||||
.ok();
|
.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 {
|
impl Device for IkeaOutlet {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.info.identifier()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for IkeaOutlet {
|
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) {
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
// Check if the message is from the deviec itself or from a remote
|
// Check if the message is from the deviec itself or from a remote
|
||||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||||
|
@ -116,7 +122,7 @@ impl OnMqtt for IkeaOutlet {
|
||||||
let state = match OnOffMessage::try_from(message) {
|
let state = match OnOffMessage::try_from(message) {
|
||||||
Ok(state) => state.state(),
|
Ok(state) => state.state(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -129,23 +135,18 @@ impl OnMqtt for IkeaOutlet {
|
||||||
// Abort any timer that is currently running
|
// Abort any timer that is currently running
|
||||||
self.stop_timeout().await.unwrap();
|
self.stop_timeout().await.unwrap();
|
||||||
|
|
||||||
debug!(id = Device::get_id(self), "Updating state to {state}");
|
debug!(id = self.identifier, "Updating state to {state}");
|
||||||
self.last_known_state = state;
|
self.last_known_state = state;
|
||||||
|
|
||||||
// If this is a kettle start a timeout for turning it of again
|
// If this is a kettle start a timeout for turning it of again
|
||||||
if state && let Some(timeout) = self.config.timeout {
|
if state && let Some(timeout) = self.config.timeout {
|
||||||
self.start_timeout(timeout).await.unwrap();
|
self.start_timeout(timeout).await.unwrap();
|
||||||
}
|
}
|
||||||
} else if self
|
} else {
|
||||||
.config
|
|
||||||
.remotes
|
|
||||||
.iter()
|
|
||||||
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
|
|
||||||
{
|
|
||||||
let action = match RemoteMessage::try_from(message) {
|
let action = match RemoteMessage::try_from(message) {
|
||||||
Ok(message) => message.action(),
|
Ok(message) => message.action(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -165,7 +166,7 @@ impl OnPresence for IkeaOutlet {
|
||||||
async fn on_presence(&mut self, presence: bool) {
|
async fn on_presence(&mut self, presence: bool) {
|
||||||
// Turn off the outlet when we leave the house (Not if it is a battery charger)
|
// Turn off the outlet when we leave the house (Not if it is a battery charger)
|
||||||
if !presence && self.config.outlet_type != OutletType::Charger {
|
if !presence && self.config.outlet_type != OutletType::Charger {
|
||||||
debug!(id = Device::get_id(self), "Turning device off");
|
debug!(id = self.identifier, "Turning device off");
|
||||||
self.set_on(false).await.ok();
|
self.set_on(false).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +186,7 @@ impl GoogleHomeDevice for IkeaOutlet {
|
||||||
device::Name::new(&self.config.info.name)
|
device::Name::new(&self.config.info.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
Device::get_id(self)
|
Device::get_id(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,13 +228,13 @@ impl crate::traits::Timeout for IkeaOutlet {
|
||||||
// get dropped
|
// get dropped
|
||||||
let client = self.config.client.clone();
|
let client = self.config.client.clone();
|
||||||
let topic = self.config.mqtt.topic.clone();
|
let topic = self.config.mqtt.topic.clone();
|
||||||
let id = Device::get_id(self).clone();
|
let id = self.identifier.clone();
|
||||||
self.handle = Some(tokio::spawn(async move {
|
self.handle = Some(tokio::spawn(async move {
|
||||||
debug!(id, "Starting timeout ({timeout:?})...");
|
debug!(id, "Starting timeout ({timeout:?})...");
|
||||||
tokio::time::sleep(timeout).await;
|
tokio::time::sleep(timeout).await;
|
||||||
debug!(id, "Turning outlet off!");
|
debug!(id, "Turning outlet off!");
|
||||||
// TODO: Idealy we would call self.set_on(false), however since we want to do
|
// 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 separate task.
|
// it after a timeout we have to put it in a seperate task.
|
||||||
// I don't think we can really get around calling outside function
|
// I don't think we can really get around calling outside function
|
||||||
set_on(client, &topic, false).await;
|
set_on(client, &topic, false).await;
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::convert::Infallible;
|
use std::net::SocketAddr;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
|
||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use bytes::{Buf, BufMut};
|
use bytes::{Buf, BufMut};
|
||||||
use google_home::errors::{self, DeviceError};
|
use google_home::errors::{self, DeviceError};
|
||||||
use google_home::traits;
|
use google_home::traits;
|
||||||
|
@ -13,34 +12,40 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::Device;
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct KasaOutletConfig {
|
pub struct KasaOutletConfig {
|
||||||
pub identifier: String,
|
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
|
||||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 9999)))]
|
addr: SocketAddr,
|
||||||
pub 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct KasaOutlet {
|
pub struct KasaOutlet {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: KasaOutletConfig,
|
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 {
|
impl Device for KasaOutlet {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,70 +1,65 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||||
use crate::messages::BrightnessMessage;
|
use crate::messages::BrightnessMessage;
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct LightSensorConfig {
|
pub struct LightSensorConfig {
|
||||||
pub identifier: String,
|
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
pub mqtt: MqttDeviceConfig,
|
||||||
pub min: isize,
|
pub min: isize,
|
||||||
pub max: isize,
|
pub max: isize,
|
||||||
#[device_config(rename("event_channel"), from(EventChannel), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
||||||
pub tx: event::Sender,
|
pub tx: event::Sender,
|
||||||
#[device_config(from_lua)]
|
|
||||||
pub client: WrappedAsyncClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT: bool = false;
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct LightSensor {
|
pub struct LightSensor {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: LightSensorConfig,
|
config: LightSensorConfig,
|
||||||
|
|
||||||
is_dark: bool,
|
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 {
|
impl Device for LightSensor {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for LightSensor {
|
impl OnMqtt for LightSensor {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let illuminance = match BrightnessMessage::try_from(message) {
|
let illuminance = match BrightnessMessage::try_from(message) {
|
||||||
Ok(state) => state.illuminance(),
|
Ok(state) => state.illuminance(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
@ -3,16 +3,15 @@ mod audio_setup;
|
||||||
mod contact_sensor;
|
mod contact_sensor;
|
||||||
mod debug_bridge;
|
mod debug_bridge;
|
||||||
mod hue_bridge;
|
mod hue_bridge;
|
||||||
mod hue_group;
|
mod hue_light;
|
||||||
mod ikea_outlet;
|
mod ikea_outlet;
|
||||||
mod kasa_outlet;
|
mod kasa_outlet;
|
||||||
mod light_sensor;
|
mod light_sensor;
|
||||||
pub mod ntfy;
|
mod ntfy;
|
||||||
mod presence;
|
mod presence;
|
||||||
mod wake_on_lan;
|
mod wake_on_lan;
|
||||||
mod washer;
|
mod washer;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use google_home::device::AsGoogleHomeDevice;
|
use google_home::device::AsGoogleHomeDevice;
|
||||||
use google_home::traits::OnOff;
|
use google_home::traits::OnOff;
|
||||||
|
|
||||||
|
@ -21,46 +20,18 @@ pub use self::audio_setup::*;
|
||||||
pub use self::contact_sensor::*;
|
pub use self::contact_sensor::*;
|
||||||
pub use self::debug_bridge::*;
|
pub use self::debug_bridge::*;
|
||||||
pub use self::hue_bridge::*;
|
pub use self::hue_bridge::*;
|
||||||
pub use self::hue_group::*;
|
pub use self::hue_light::*;
|
||||||
pub use self::ikea_outlet::*;
|
pub use self::ikea_outlet::*;
|
||||||
pub use self::kasa_outlet::*;
|
pub use self::kasa_outlet::*;
|
||||||
pub use self::light_sensor::*;
|
pub use self::light_sensor::*;
|
||||||
pub use self::ntfy::{Ntfy, NtfyConfig};
|
pub use self::ntfy::{Notification, Ntfy};
|
||||||
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
|
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
|
||||||
pub use self::wake_on_lan::*;
|
pub use self::wake_on_lan::*;
|
||||||
pub use self::washer::*;
|
pub use self::washer::*;
|
||||||
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||||
use crate::traits::Timeout;
|
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)]
|
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
|
||||||
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
|
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
|
||||||
fn get_id(&self) -> String;
|
fn get_id(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_repr::*;
|
use serde_repr::*;
|
||||||
use tracing::{error, trace, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
use crate::config::NtfyConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
|
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)]
|
#[derive(Debug, Serialize_repr, Clone, Copy)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Priority {
|
pub enum Priority {
|
||||||
|
@ -35,9 +40,9 @@ pub enum ActionType {
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub action: ActionType,
|
action: ActionType,
|
||||||
pub label: String,
|
label: String,
|
||||||
pub clear: Option<bool>,
|
clear: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -111,50 +116,28 @@ impl Default for Notification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 {
|
impl Ntfy {
|
||||||
|
pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self {
|
||||||
|
Self {
|
||||||
|
base_url: config.url,
|
||||||
|
topic: config.topic,
|
||||||
|
tx: event_channel.get_tx(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send(&self, notification: Notification) {
|
async fn send(&self, notification: Notification) {
|
||||||
let notification = notification.finalize(&self.config.topic);
|
let notification = notification.finalize(&self.topic);
|
||||||
|
debug!("Sending notfication");
|
||||||
|
|
||||||
// Create the request
|
// Create the request
|
||||||
let res = reqwest::Client::new()
|
let res = reqwest::Client::new()
|
||||||
.post(self.config.url.clone())
|
.post(self.base_url.clone())
|
||||||
.json(¬ification)
|
.json(¬ification)
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("Something went wrong while sending the notification: {err}");
|
error!("Something went wrong while sending the notifcation: {err}");
|
||||||
} else if let Ok(res) = res {
|
} else if let Ok(res) = res {
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
|
@ -164,6 +147,12 @@ impl Ntfy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Device for Ntfy {
|
||||||
|
fn get_id(&self) -> &str {
|
||||||
|
"ntfy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnPresence for Ntfy {
|
impl OnPresence for Ntfy {
|
||||||
async fn on_presence(&mut self, presence: bool) {
|
async fn on_presence(&mut self, presence: bool) {
|
||||||
|
@ -188,13 +177,7 @@ impl OnPresence for Ntfy {
|
||||||
.add_action(action)
|
.add_action(action)
|
||||||
.set_priority(Priority::Low);
|
.set_priority(Priority::Low);
|
||||||
|
|
||||||
if self
|
if self.tx.send(Event::Ntfy(notification)).await.is_err() {
|
||||||
.config
|
|
||||||
.tx
|
|
||||||
.send(Event::Ntfy(notification))
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
warn!("There are no receivers on the event channel");
|
warn!("There are no receivers on the event channel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +1,60 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, trace, warn};
|
use serde::Deserialize;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use super::LuaDeviceCreate;
|
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
use crate::devices::Device;
|
use crate::devices::Device;
|
||||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||||
use crate::messages::PresenceMessage;
|
use crate::messages::PresenceMessage;
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
|
|
||||||
#[derive(Debug, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PresenceConfig {
|
pub struct PresenceConfig {
|
||||||
#[device_config(flatten)]
|
#[serde(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
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;
|
pub const DEFAULT_PRESENCE: bool = false;
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug)]
|
||||||
pub struct Presence {
|
pub struct Presence {
|
||||||
config: PresenceConfig,
|
tx: event::Sender,
|
||||||
|
mqtt: MqttDeviceConfig,
|
||||||
devices: HashMap<String, bool>,
|
devices: HashMap<String, bool>,
|
||||||
current_overall_presence: bool,
|
current_overall_presence: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl Presence {
|
||||||
impl LuaDeviceCreate for Presence {
|
pub fn new(config: PresenceConfig, event_channel: &EventChannel) -> Self {
|
||||||
type Config = PresenceConfig;
|
Self {
|
||||||
type Error = rumqttc::ClientError;
|
tx: event_channel.get_tx(),
|
||||||
|
mqtt: config.mqtt,
|
||||||
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(),
|
devices: HashMap::new(),
|
||||||
current_overall_presence: DEFAULT_PRESENCE,
|
current_overall_presence: DEFAULT_PRESENCE,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device for Presence {
|
impl Device for Presence {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
"presence".to_string()
|
"presence"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for Presence {
|
impl OnMqtt for Presence {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let offset = self
|
let offset = self
|
||||||
.config
|
|
||||||
.mqtt
|
.mqtt
|
||||||
.topic
|
.topic
|
||||||
.find('+')
|
.find('+')
|
||||||
.or(self.config.mqtt.topic.find('#'))
|
.or(self.mqtt.topic.find('#'))
|
||||||
.expect("Presence::create fails if it does not contain wildcards");
|
.expect("Presence::create fails if it does not contain wildcards");
|
||||||
let device_name = message.topic[offset..].into();
|
let device_name = message.topic[offset..].into();
|
||||||
|
|
||||||
|
@ -97,7 +81,6 @@ impl OnMqtt for Presence {
|
||||||
self.current_overall_presence = overall_presence;
|
self.current_overall_presence = overall_presence;
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.config
|
|
||||||
.tx
|
.tx
|
||||||
.send(Event::Presence(overall_presence))
|
.send(Event::Presence(overall_presence))
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use eui48::MacAddress;
|
use eui48::MacAddress;
|
||||||
use google_home::errors::ErrorCode;
|
use google_home::errors::ErrorCode;
|
||||||
use google_home::traits::{self, Scene};
|
use google_home::traits::{self, Scene};
|
||||||
|
@ -10,64 +10,69 @@ use google_home::{device, GoogleHomeDevice};
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::Device;
|
||||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||||
|
use crate::device_manager::DeviceConfig;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::OnMqtt;
|
use crate::event::OnMqtt;
|
||||||
use crate::messages::ActivateMessage;
|
use crate::messages::ActivateMessage;
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct WakeOnLANConfig {
|
pub struct WakeOnLANConfig {
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub info: InfoConfig,
|
info: InfoConfig,
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
pub mac_address: MacAddress,
|
mac_address: MacAddress,
|
||||||
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
|
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
|
||||||
pub broadcast_ip: Ipv4Addr,
|
broadcast_ip: Ipv4Addr,
|
||||||
#[device_config(from_lua)]
|
}
|
||||||
pub client: WrappedAsyncClient,
|
|
||||||
|
#[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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct WakeOnLAN {
|
pub struct WakeOnLAN {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: WakeOnLANConfig,
|
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 {
|
impl Device for WakeOnLAN {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.info.identifier()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for WakeOnLAN {
|
impl OnMqtt for WakeOnLAN {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let activate = match ActivateMessage::try_from(message) {
|
let activate = match ActivateMessage::try_from(message) {
|
||||||
Ok(message) => message.activate(),
|
Ok(message) => message.activate(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -88,7 +93,7 @@ impl GoogleHomeDevice for WakeOnLAN {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
Device::get_id(self)
|
Device::get_id(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +111,14 @@ impl traits::Scene for WakeOnLAN {
|
||||||
async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> {
|
async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> {
|
||||||
if activate {
|
if activate {
|
||||||
debug!(
|
debug!(
|
||||||
id = Device::get_id(self),
|
id = self.identifier,
|
||||||
"Activating Computer: {} (Sending to {})",
|
"Activating Computer: {} (Sending to {})",
|
||||||
self.config.mac_address,
|
self.config.mac_address,
|
||||||
self.config.broadcast_ip
|
self.config.broadcast_ip
|
||||||
);
|
);
|
||||||
let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
|
let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
|
||||||
|err| {
|
|err| {
|
||||||
error!(id = Device::get_id(self), "invalid mac address: {err}");
|
error!(id = self.identifier, "invalid mac address: {err}");
|
||||||
google_home::errors::DeviceError::TransientError
|
google_home::errors::DeviceError::TransientError
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
@ -124,16 +129,13 @@ impl traits::Scene for WakeOnLAN {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to activate computer: {err}");
|
||||||
id = Device::get_id(self),
|
|
||||||
"Failed to activate computer: {err}"
|
|
||||||
);
|
|
||||||
google_home::errors::DeviceError::TransientError.into()
|
google_home::errors::DeviceError::TransientError.into()
|
||||||
})
|
})
|
||||||
.map(|_| debug!(id = Device::get_id(self), "Success!"))
|
.map(|_| debug!(id = self.identifier, "Success!"))
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
id = Device::get_id(self),
|
id = self.identifier,
|
||||||
"Trying to deactivate computer, this is not currently supported"
|
"Trying to deactivate computer, this is not currently supported"
|
||||||
);
|
);
|
||||||
// We do not support deactivating this scene
|
// We do not support deactivating this scene
|
||||||
|
|
|
@ -1,79 +1,72 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use super::ntfy::Priority;
|
use super::ntfy::Priority;
|
||||||
use super::{Device, LuaDeviceCreate};
|
use super::{Device, Notification};
|
||||||
use crate::config::MqttDeviceConfig;
|
use crate::config::MqttDeviceConfig;
|
||||||
use crate::devices::ntfy::Notification;
|
use crate::device_manager::DeviceConfig;
|
||||||
|
use crate::error::DeviceConfigError;
|
||||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||||
use crate::messages::PowerMessage;
|
use crate::messages::PowerMessage;
|
||||||
use crate::mqtt::WrappedAsyncClient;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||||
pub struct WasherConfig {
|
pub struct WasherConfig {
|
||||||
pub identifier: String,
|
|
||||||
#[device_config(flatten)]
|
#[device_config(flatten)]
|
||||||
pub mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
// Power in Watt
|
// Power in Watt
|
||||||
pub threshold: f32,
|
threshold: f32,
|
||||||
#[device_config(rename("event_channel"), from_lua, from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
|
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
||||||
pub tx: event::Sender,
|
pub tx: event::Sender,
|
||||||
#[device_config(from_lua)]
|
}
|
||||||
pub client: WrappedAsyncClient,
|
|
||||||
|
#[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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add google home integration
|
// TODO: Add google home integration
|
||||||
|
|
||||||
#[derive(Debug, LuaDevice)]
|
#[derive(Debug, LuaDevice)]
|
||||||
pub struct Washer {
|
pub struct Washer {
|
||||||
|
identifier: String,
|
||||||
|
#[config]
|
||||||
config: WasherConfig,
|
config: WasherConfig,
|
||||||
|
|
||||||
running: isize,
|
running: isize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device for Washer {
|
impl Device for Washer {
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> &str {
|
||||||
self.config.identifier.clone()
|
&self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The washer needs to have a power draw above the threshold multiple times before the washer is
|
// The washer needs to have a power draw above the theshold multiple times before the washer is
|
||||||
// actually marked as running
|
// actually marked as running
|
||||||
// This helps prevent false positives
|
// This helps prevent false positives
|
||||||
const HYSTERESIS: isize = 10;
|
const HYSTERESIS: isize = 10;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl OnMqtt for Washer {
|
impl OnMqtt for Washer {
|
||||||
async fn on_mqtt(&mut self, message: Publish) {
|
fn topics(&self) -> Vec<&str> {
|
||||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
vec![&self.config.mqtt.topic]
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async fn on_mqtt(&mut self, message: Publish) {
|
||||||
let power = match PowerMessage::try_from(message) {
|
let power = match PowerMessage::try_from(message) {
|
||||||
Ok(state) => state.power(),
|
Ok(state) => state.power(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||||
id = self.config.identifier,
|
|
||||||
"Failed to parse message: {err}"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -83,7 +76,7 @@ impl OnMqtt for Washer {
|
||||||
if power < self.config.threshold && self.running >= HYSTERESIS {
|
if power < self.config.threshold && self.running >= HYSTERESIS {
|
||||||
// The washer is done running
|
// The washer is done running
|
||||||
debug!(
|
debug!(
|
||||||
id = self.config.identifier,
|
id = self.identifier,
|
||||||
power,
|
power,
|
||||||
threshold = self.config.threshold,
|
threshold = self.config.threshold,
|
||||||
"Washer is done"
|
"Washer is done"
|
||||||
|
@ -111,7 +104,7 @@ impl OnMqtt for Washer {
|
||||||
} else if power >= self.config.threshold && self.running < HYSTERESIS {
|
} else if power >= self.config.threshold && self.running < HYSTERESIS {
|
||||||
// Washer could be starting
|
// Washer could be starting
|
||||||
debug!(
|
debug!(
|
||||||
id = self.config.identifier,
|
id = self.identifier,
|
||||||
power,
|
power,
|
||||||
threshold = self.config.threshold,
|
threshold = self.config.threshold,
|
||||||
"Washer is starting"
|
"Washer is starting"
|
||||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -65,6 +65,16 @@ pub enum ParseError {
|
||||||
InvalidPayload(Bytes),
|
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
|
// TODO: Would be nice to somehow get the line number of the expected wildcard topic
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("Topic '{topic}' is expected to be a wildcard topic")]
|
#[error("Topic '{topic}' is expected to be a wildcard topic")]
|
||||||
|
@ -82,10 +92,12 @@ impl MissingWildcard {
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DeviceConfigError {
|
pub enum DeviceConfigError {
|
||||||
|
#[error("Child '{1}' of device '{0}' does not exist")]
|
||||||
|
MissingChild(String, String),
|
||||||
#[error("Device '{0}' does not implement expected trait '{1}'")]
|
#[error("Device '{0}' does not implement expected trait '{1}'")]
|
||||||
MissingTrait(String, String),
|
MissingTrait(String, String),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
MqttClientError(#[from] rumqttc::ClientError),
|
MissingWildcard(#[from] MissingWildcard),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
@ -4,7 +4,7 @@ use mlua::FromLua;
|
||||||
use rumqttc::Publish;
|
use rumqttc::Publish;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::devices::ntfy::Notification;
|
use crate::devices::Notification;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
@ -37,7 +37,7 @@ impl mlua::UserData for EventChannel {}
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
#[device_trait]
|
#[device_trait]
|
||||||
pub trait OnMqtt {
|
pub trait OnMqtt {
|
||||||
// fn topics(&self) -> Vec<&str>;
|
fn topics(&self) -> Vec<&str>;
|
||||||
async fn on_mqtt(&mut self, message: Publish);
|
async fn on_mqtt(&mut self, message: Publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
#![allow(incomplete_features)]
|
#![allow(incomplete_features)]
|
||||||
#![feature(specialization)]
|
#![feature(specialization)]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod device_manager;
|
pub mod device_manager;
|
||||||
|
@ -14,5 +11,3 @@ pub mod messages;
|
||||||
pub mod mqtt;
|
pub mod mqtt;
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|
||||||
pub static LUA: Lazy<Mutex<mlua::Lua>> = Lazy::new(|| Mutex::new(mlua::Lua::new()));
|
|
||||||
|
|
112
src/main.rs
112
src/main.rs
|
@ -1,14 +1,15 @@
|
||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
use std::path::Path;
|
use std::{fs, process};
|
||||||
use std::process;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use automation::auth::{OpenIDConfig, User};
|
||||||
use automation::auth::User;
|
use automation::config::Config;
|
||||||
use automation::config::{FulfillmentConfig, MqttConfig};
|
|
||||||
use automation::device_manager::DeviceManager;
|
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::error::ApiError;
|
||||||
use automation::mqtt::{self, WrappedAsyncClient};
|
use automation::mqtt::{self, WrappedAsyncClient};
|
||||||
use automation::{devices, LUA};
|
|
||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
@ -16,18 +17,17 @@ use axum::routing::post;
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use google_home::{GoogleHome, Request};
|
use google_home::{GoogleHome, Request};
|
||||||
use mlua::LuaSerdeExt;
|
|
||||||
use rumqttc::AsyncClient;
|
use rumqttc::AsyncClient;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
pub openid_url: String,
|
pub openid: OpenIDConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for String {
|
impl FromRef<AppState> for OpenIDConfig {
|
||||||
fn from_ref(input: &AppState) -> Self {
|
fn from_ref(input: &AppState) -> Self {
|
||||||
input.openid_url.clone()
|
input.openid.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,32 +51,41 @@ async fn app() -> anyhow::Result<()> {
|
||||||
|
|
||||||
info!("Starting automation_rs...");
|
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
|
// Setup the device handler
|
||||||
let device_manager = DeviceManager::new().await;
|
let device_manager = DeviceManager::new(client.clone());
|
||||||
|
|
||||||
let fulfillment_config = {
|
device_manager.add_schedule(config.schedule).await;
|
||||||
let lua = LUA.lock().await;
|
|
||||||
|
|
||||||
lua.set_warning_function(|_lua, text, _cont| {
|
let event_channel = device_manager.event_channel();
|
||||||
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 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("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 util = lua.create_table()?;
|
||||||
let get_env = lua.create_function(|_lua, name: String| {
|
let get_env = lua.create_function(|_lua, name: String| {
|
||||||
|
@ -87,32 +96,37 @@ async fn app() -> anyhow::Result<()> {
|
||||||
|
|
||||||
lua.globals().set("automation", automation)?;
|
lua.globals().set("automation", automation)?;
|
||||||
|
|
||||||
devices::register_with_lua(&lua)?;
|
// 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)?;
|
||||||
|
|
||||||
// TODO: Make this not hardcoded
|
// TODO: Make this not hardcoded
|
||||||
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());
|
let filename = "config.lua";
|
||||||
let config_path = Path::new(&config_filename);
|
let file = fs::read_to_string(filename)?;
|
||||||
match lua.load(config_path).exec_async().await {
|
match lua.load(file).set_name(filename).exec_async().await {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
println!("{error}");
|
println!("{error}");
|
||||||
Err(error)
|
Err(error)
|
||||||
}
|
}
|
||||||
result => result,
|
result => result,
|
||||||
}?;
|
}?;
|
||||||
|
}
|
||||||
|
|
||||||
let automation: mlua::Table = lua.globals().get("automation")?;
|
// Wrap the mqtt eventloop and start listening for message
|
||||||
let fulfillment_config: Option<mlua::Value> = automation.get("fulfillment")?;
|
// NOTE: We wait until all the setup is done, as otherwise we might miss some messages
|
||||||
if let Some(fulfillment_config) = fulfillment_config {
|
mqtt::start(eventloop, &event_channel);
|
||||||
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
|
// Create google home fullfillment route
|
||||||
let fulfillment = Router::new().route(
|
let fullfillment = Router::new().route(
|
||||||
"/google_home",
|
"/google_home",
|
||||||
post(async move |user: User, Json(payload): Json<Request>| {
|
post(async move |user: User, Json(payload): Json<Request>| {
|
||||||
debug!(username = user.preferred_username, "{payload:#?}");
|
debug!(username = user.preferred_username, "{payload:#?}");
|
||||||
|
@ -134,13 +148,13 @@ async fn app() -> anyhow::Result<()> {
|
||||||
|
|
||||||
// Combine together all the routes
|
// Combine together all the routes
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/fulfillment", fulfillment)
|
.nest("/fullfillment", fullfillment)
|
||||||
.with_state(AppState {
|
.with_state(AppState {
|
||||||
openid_url: fulfillment_config.openid_url.clone(),
|
openid: config.openid,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
let addr = fulfillment_config.into();
|
let addr = config.fullfillment.into();
|
||||||
info!("Server started on http://{addr}");
|
info!("Server started on http://{addr}");
|
||||||
axum::Server::try_bind(&addr)?
|
axum::Server::try_bind(&addr)?
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
|
|
Loading…
Reference in New Issue
Block a user