Reorganized project
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use mlua::{FromLua, IntoLuaMulti};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Internal {
|
||||
uuid: uuid::Uuid,
|
||||
lua: mlua::Lua,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ActionCallback<T> {
|
||||
internal: Option<Internal>,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for ActionCallback<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
internal: None,
|
||||
phantom: PhantomData::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromLua for ActionCallback<T> {
|
||||
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
lua.set_named_registry_value(&uuid.to_string(), value)?;
|
||||
|
||||
Ok(ActionCallback {
|
||||
internal: Some(Internal {
|
||||
uuid,
|
||||
lua: lua.clone(),
|
||||
}),
|
||||
phantom: PhantomData::<T>,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Return proper error here
|
||||
impl<T> ActionCallback<T>
|
||||
where
|
||||
T: IntoLuaMulti + Sync + Send + Clone + 'static,
|
||||
{
|
||||
pub async fn call(&self, state: T) {
|
||||
let Some(internal) = self.internal.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let callback: mlua::Value = internal
|
||||
.lua
|
||||
.named_registry_value(&internal.uuid.to_string())
|
||||
.unwrap();
|
||||
match callback {
|
||||
mlua::Value::Function(f) => f.call_async::<()>(state).await.unwrap(),
|
||||
_ => todo!("Only functions are currently supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use rumqttc::{MqttOptions, Transport};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MqttConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub client_name: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub tls: bool,
|
||||
}
|
||||
|
||||
impl From<MqttConfig> for MqttOptions {
|
||||
fn from(value: MqttConfig) -> Self {
|
||||
let mut mqtt_options = MqttOptions::new(value.client_name, value.host, value.port);
|
||||
mqtt_options.set_credentials(value.username, value.password);
|
||||
mqtt_options.set_keep_alive(Duration::from_secs(5));
|
||||
|
||||
if value.tls {
|
||||
mqtt_options.set_transport(Transport::tls_with_default_config());
|
||||
}
|
||||
|
||||
mqtt_options
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FulfillmentConfig {
|
||||
pub openid_url: String,
|
||||
#[serde(default = "default_fulfillment_ip")]
|
||||
pub ip: Ipv4Addr,
|
||||
#[serde(default = "default_fulfillment_port")]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl From<FulfillmentConfig> for SocketAddr {
|
||||
fn from(fulfillment: FulfillmentConfig) -> Self {
|
||||
(fulfillment.ip, fulfillment.port).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_fulfillment_ip() -> Ipv4Addr {
|
||||
[0, 0, 0, 0].into()
|
||||
}
|
||||
|
||||
fn default_fulfillment_port() -> u16 {
|
||||
7878
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct InfoConfig {
|
||||
pub name: String,
|
||||
pub room: Option<String>,
|
||||
}
|
||||
|
||||
impl InfoConfig {
|
||||
pub fn identifier(&self) -> String {
|
||||
(if let Some(room) = &self.room {
|
||||
room.to_ascii_lowercase().replace(' ', "_") + "_"
|
||||
} else {
|
||||
String::new()
|
||||
}) + &self.name.to_ascii_lowercase().replace(' ', "_")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MqttDeviceConfig {
|
||||
pub topic: String,
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::future::join_all;
|
||||
use futures::Future;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use tracing::{debug, instrument, trace};
|
||||
|
||||
use crate::devices::Device;
|
||||
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
|
||||
pub type DeviceMap = HashMap<String, Box<dyn Device>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceManager {
|
||||
devices: Arc<RwLock<DeviceMap>>,
|
||||
event_channel: EventChannel,
|
||||
scheduler: JobScheduler,
|
||||
}
|
||||
|
||||
impl DeviceManager {
|
||||
pub async fn new() -> Self {
|
||||
let (event_channel, mut event_rx) = EventChannel::new();
|
||||
|
||||
let device_manager = Self {
|
||||
devices: Arc::new(RwLock::new(HashMap::new())),
|
||||
event_channel,
|
||||
scheduler: JobScheduler::new().await.unwrap(),
|
||||
};
|
||||
|
||||
tokio::spawn({
|
||||
let device_manager = device_manager.clone();
|
||||
async move {
|
||||
loop {
|
||||
if let Some(event) = event_rx.recv().await {
|
||||
device_manager.handle_event(event).await;
|
||||
} else {
|
||||
todo!("Handle errors with the event channel properly")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
device_manager.scheduler.start().await.unwrap();
|
||||
|
||||
device_manager
|
||||
}
|
||||
|
||||
pub async fn add(&self, device: Box<dyn Device>) {
|
||||
let id = device.get_id();
|
||||
|
||||
debug!(id, "Adding device");
|
||||
|
||||
self.devices.write().await.insert(id, device);
|
||||
}
|
||||
|
||||
pub fn event_channel(&self) -> EventChannel {
|
||||
self.event_channel.clone()
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<Box<dyn Device>> {
|
||||
self.devices.read().await.get(name).cloned()
|
||||
}
|
||||
|
||||
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
|
||||
self.devices.read().await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn handle_event(&self, event: Event) {
|
||||
match event {
|
||||
Event::MqttMessage(message) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| {
|
||||
let message = message.clone();
|
||||
async move {
|
||||
let device: Option<&dyn OnMqtt> = device.cast();
|
||||
if let Some(device) = device {
|
||||
// let subscribed = device
|
||||
// .topics()
|
||||
// .iter()
|
||||
// .any(|topic| matches(&message.topic, topic));
|
||||
//
|
||||
// if subscribed {
|
||||
trace!(id, "Handling");
|
||||
device.on_mqtt(message).await;
|
||||
trace!(id, "Done");
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
join_all(iter).await;
|
||||
}
|
||||
Event::Darkness(dark) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let device: Option<&dyn OnDarkness> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_darkness(dark).await;
|
||||
trace!(id, "Done");
|
||||
}
|
||||
});
|
||||
|
||||
join_all(iter).await;
|
||||
}
|
||||
Event::Presence(presence) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let device: Option<&dyn OnPresence> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_presence(presence).await;
|
||||
trace!(id, "Done");
|
||||
}
|
||||
});
|
||||
|
||||
join_all(iter).await;
|
||||
}
|
||||
Event::Ntfy(notification) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| {
|
||||
let notification = notification.clone();
|
||||
async move {
|
||||
let device: Option<&dyn OnNotification> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_notification(notification).await;
|
||||
trace!(id, "Done");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
join_all(iter).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl mlua::UserData for DeviceManager {
|
||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_async_method("add", |_lua, this, device: Box<dyn Device>| async move {
|
||||
this.add(device).await;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
methods.add_async_method(
|
||||
"schedule",
|
||||
|lua, this, (schedule, f): (String, mlua::Function)| async move {
|
||||
debug!("schedule = {schedule}");
|
||||
// This creates a function, that returns the actual job we want to run
|
||||
let create_job = {
|
||||
let lua = lua.clone();
|
||||
|
||||
move |uuid: uuid::Uuid,
|
||||
_: tokio_cron_scheduler::JobScheduler|
|
||||
-> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let lua = lua.clone();
|
||||
|
||||
// Create the actual function we want to run on a schedule
|
||||
let future = async move {
|
||||
let f: mlua::Function =
|
||||
lua.named_registry_value(uuid.to_string().as_str()).unwrap();
|
||||
f.call_async::<()>(()).await.unwrap();
|
||||
};
|
||||
|
||||
Box::pin(future)
|
||||
}
|
||||
};
|
||||
|
||||
let job = Job::new_async(schedule.as_str(), create_job).unwrap();
|
||||
|
||||
let uuid = this.scheduler.add(job).await.unwrap();
|
||||
|
||||
// Store the function in the registry
|
||||
lua.set_named_registry_value(uuid.to_string().as_str(), f)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel()))
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::device::Name;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{
|
||||
AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValue, TemperatureSetting,
|
||||
TemperatureUnit,
|
||||
};
|
||||
use google_home::types::Type;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AirFilter {
|
||||
config: Config,
|
||||
state: Arc<RwLock<AirFilterState>>,
|
||||
}
|
||||
|
||||
impl AirFilter {
|
||||
async fn set_speed(&self, state: AirFilterFanState) {
|
||||
let message = SetAirFilterFanState::new(state);
|
||||
|
||||
let topic = format!("{}/set", self.config.mqtt.topic);
|
||||
// TODO: Handle potential errors here
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
}
|
||||
|
||||
async fn state(&self) -> RwLockReadGuard<AirFilterState> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<AirFilterState> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for AirFilter {
|
||||
type Config = Config;
|
||||
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?;
|
||||
|
||||
let state = AirFilterState {
|
||||
state: AirFilterFanState::Off,
|
||||
humidity: 0.0,
|
||||
temperature: 0.0,
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for AirFilter {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for AirFilter {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = match AirFilterState::try_from(message) {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if state == *self.state().await {
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(id = Device::get_id(self), "Updating state to {state:?}");
|
||||
|
||||
*self.state_mut().await = state;
|
||||
}
|
||||
}
|
||||
|
||||
impl google_home::Device for AirFilter {
|
||||
fn get_device_type(&self) -> Type {
|
||||
Type::AirPurifier
|
||||
}
|
||||
|
||||
fn get_device_name(&self) -> Name {
|
||||
Name::new(&self.config.info.name)
|
||||
}
|
||||
|
||||
fn get_id(&self) -> String {
|
||||
Device::get_id(self)
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_room_hint(&self) -> Option<&str> {
|
||||
self.config.info.room.as_deref()
|
||||
}
|
||||
|
||||
fn will_report_state(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnOff for AirFilter {
|
||||
async fn on(&self) -> Result<bool, ErrorCode> {
|
||||
Ok(self.state().await.state != AirFilterFanState::Off)
|
||||
}
|
||||
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
debug!("Turning on air filter: {on}");
|
||||
|
||||
if on {
|
||||
self.set_speed(AirFilterFanState::High).await;
|
||||
} else {
|
||||
self.set_speed(AirFilterFanState::Off).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FanSpeed for AirFilter {
|
||||
fn available_fan_speeds(&self) -> AvailableSpeeds {
|
||||
AvailableSpeeds {
|
||||
speeds: vec![
|
||||
Speed {
|
||||
speed_name: "off".into(),
|
||||
speed_values: vec![SpeedValue {
|
||||
speed_synonym: vec!["Off".into()],
|
||||
lang: "en".into(),
|
||||
}],
|
||||
},
|
||||
Speed {
|
||||
speed_name: "low".into(),
|
||||
speed_values: vec![SpeedValue {
|
||||
speed_synonym: vec!["Low".into()],
|
||||
lang: "en".into(),
|
||||
}],
|
||||
},
|
||||
Speed {
|
||||
speed_name: "medium".into(),
|
||||
speed_values: vec![SpeedValue {
|
||||
speed_synonym: vec!["Medium".into()],
|
||||
lang: "en".into(),
|
||||
}],
|
||||
},
|
||||
Speed {
|
||||
speed_name: "high".into(),
|
||||
speed_values: vec![SpeedValue {
|
||||
speed_synonym: vec!["High".into()],
|
||||
lang: "en".into(),
|
||||
}],
|
||||
},
|
||||
],
|
||||
ordered: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn current_fan_speed_setting(&self) -> Result<String, ErrorCode> {
|
||||
let speed = match self.state().await.state {
|
||||
AirFilterFanState::Off => "off",
|
||||
AirFilterFanState::Low => "low",
|
||||
AirFilterFanState::Medium => "medium",
|
||||
AirFilterFanState::High => "high",
|
||||
};
|
||||
|
||||
Ok(speed.into())
|
||||
}
|
||||
|
||||
async fn set_fan_speed(&self, fan_speed: String) -> Result<(), ErrorCode> {
|
||||
let fan_speed = fan_speed.as_str();
|
||||
let state = if fan_speed == "off" {
|
||||
AirFilterFanState::Off
|
||||
} else if fan_speed == "low" {
|
||||
AirFilterFanState::Low
|
||||
} else if fan_speed == "medium" {
|
||||
AirFilterFanState::Medium
|
||||
} else if fan_speed == "high" {
|
||||
AirFilterFanState::High
|
||||
} else {
|
||||
return Err(google_home::errors::DeviceError::TransientError.into());
|
||||
};
|
||||
|
||||
self.set_speed(state).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HumiditySetting for AirFilter {
|
||||
fn query_only_humidity_setting(&self) -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
async fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode> {
|
||||
Ok(self.state().await.humidity.round() as isize)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TemperatureSetting for AirFilter {
|
||||
fn query_only_temperature_control(&self) -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn temperatureUnitForUX(&self) -> TemperatureUnit {
|
||||
TemperatureUnit::Celsius
|
||||
}
|
||||
|
||||
async fn temperature_ambient_celsius(&self) -> f32 {
|
||||
// HACK: Round to one decimal place
|
||||
(10.0 * self.state().await.temperature).round() / 10.0
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::action_callback::ActionCallback;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::DEFAULT_PRESENCE;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{ContactMessage, PresenceMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
// NOTE: If we add more presence devices we might need to move this out of here
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct PresenceDeviceConfig {
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(with(Duration::from_secs))]
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua, default)]
|
||||
pub presence: Option<PresenceDeviceConfig>,
|
||||
#[device_config(from_lua, default)]
|
||||
pub callback: ActionCallback<bool>,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
overall_presence: bool,
|
||||
is_closed: bool,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactSensor {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl ContactSensor {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for ContactSensor {
|
||||
type Config = Config;
|
||||
type Error = DeviceConfigError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up ContactSensor");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
let state = State {
|
||||
overall_presence: DEFAULT_PRESENCE,
|
||||
is_closed: true,
|
||||
handle: None,
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for ContactSensor {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for ContactSensor {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
self.state_mut().await.overall_presence = presence;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for ContactSensor {
|
||||
async fn on_mqtt(&self, message: rumqttc::Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_closed = match ContactMessage::try_from(message) {
|
||||
Ok(state) => state.is_closed(),
|
||||
Err(err) => {
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if is_closed == self.state().await.is_closed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.config.callback.call(!is_closed).await;
|
||||
|
||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||
self.state_mut().await.is_closed = is_closed;
|
||||
|
||||
// Check if this contact sensor works as a presence device
|
||||
// If not we are done here
|
||||
let presence = match &self.config.presence {
|
||||
Some(presence) => presence.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !is_closed {
|
||||
// Activate presence and stop any timeout once we open the door
|
||||
if let Some(handle) = self.state_mut().await.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// Only use the door as an presence sensor if there the current presence is set false
|
||||
// This is to prevent the house from being marked as present for however long the
|
||||
// timeout is set when leaving the house
|
||||
if !self.state().await.overall_presence {
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
&presence.mqtt.topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to publish presence on {}: {err}",
|
||||
presence.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
// Once the door is closed again we start a timeout for removing the presence
|
||||
let device = self.clone();
|
||||
self.state_mut().await.handle = Some(tokio::spawn(async move {
|
||||
debug!(
|
||||
id = device.get_id(),
|
||||
"Starting timeout ({:?}) for contact sensor...", presence.timeout
|
||||
);
|
||||
tokio::time::sleep(presence.timeout).await;
|
||||
debug!(id = device.get_id(), "Removing door device!");
|
||||
device
|
||||
.config
|
||||
.client
|
||||
.publish(&presence.mqtt.topic, rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to publish presence on {}: {err}",
|
||||
presence.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{OnDarkness, OnPresence};
|
||||
use crate::messages::{DarknessMessage, PresenceMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugBridge {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for DebugBridge {
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up DebugBridge");
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for DebugBridge {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for DebugBridge {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
let message = PresenceMessage::new(presence);
|
||||
let topic = format!("{}/presence", self.config.mqtt.topic);
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
true,
|
||||
serde_json::to_string(&message).expect("Serialization should not fail"),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to update presence on {}/presence: {err}",
|
||||
self.config.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnDarkness for DebugBridge {
|
||||
async fn on_darkness(&self, dark: bool) {
|
||||
let message = DarknessMessage::new(dark);
|
||||
let topic = format!("{}/darkness", self.config.mqtt.topic);
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
true,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to update presence on {}/presence: {err}",
|
||||
self.config.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{OnDarkness, OnPresence};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Flag {
|
||||
Presence,
|
||||
Darkness,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct FlagIDs {
|
||||
presence: isize,
|
||||
darkness: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
pub login: String,
|
||||
pub flags: FlagIDs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HueBridge {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct FlagMessage {
|
||||
flag: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for HueBridge {
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Infallible> {
|
||||
trace!(id = config.identifier, "Setting up HueBridge");
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl HueBridge {
|
||||
pub async fn set_flag(&self, flag: Flag, value: bool) {
|
||||
let flag_id = match flag {
|
||||
Flag::Presence => self.config.flags.presence,
|
||||
Flag::Darkness => self.config.flags.darkness,
|
||||
};
|
||||
|
||||
let url = format!(
|
||||
"http://{}/api/{}/sensors/{flag_id}/state",
|
||||
self.config.addr, self.config.login
|
||||
);
|
||||
|
||||
trace!(?flag, flag_id, value, "Sending request to change flag");
|
||||
let res = reqwest::Client::new()
|
||||
.put(url)
|
||||
.json(&FlagMessage { flag: value })
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!(flag_id, "Status code is not success: {status}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(flag_id, "Error: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for HueBridge {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for HueBridge {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
trace!("Bridging presence to hue");
|
||||
self.set_flag(Flag::Presence, presence).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnDarkness for HueBridge {
|
||||
async fn on_darkness(&self, dark: bool) {
|
||||
trace!("Bridging darkness to hue");
|
||||
self.set_flag(Flag::Darkness, dark).await;
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::OnOff;
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
pub login: String,
|
||||
pub group_id: isize,
|
||||
pub scene_id: String,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HueGroup {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
// Couple of helper function to get the correct urls
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for HueGroup {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up AudioSetup");
|
||||
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl HueGroup {
|
||||
fn url_base(&self) -> String {
|
||||
format!("http://{}/api/{}", self.config.addr, self.config.login)
|
||||
}
|
||||
|
||||
fn url_set_action(&self) -> String {
|
||||
format!("{}/groups/{}/action", self.url_base(), self.config.group_id)
|
||||
}
|
||||
|
||||
fn url_get_state(&self) -> String {
|
||||
format!("{}/groups/{}", self.url_base(), self.config.group_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for HueGroup {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnOff for HueGroup {
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
let message = if on {
|
||||
message::Action::scene(self.config.scene_id.clone())
|
||||
} else {
|
||||
message::Action::on(false)
|
||||
};
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.put(self.url_set_action())
|
||||
.json(&message)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!(id = self.get_id(), "Status code is not success: {status}");
|
||||
}
|
||||
}
|
||||
Err(err) => error!(id = self.get_id(), "Error: {err}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on(&self) -> Result<bool, ErrorCode> {
|
||||
let res = reqwest::Client::new()
|
||||
.get(self.url_get_state())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!(id = self.get_id(), "Status code is not success: {status}");
|
||||
}
|
||||
|
||||
let on = match res.json::<message::Info>().await {
|
||||
Ok(info) => info.any_on(),
|
||||
Err(err) => {
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
// TODO: Error code
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(on);
|
||||
}
|
||||
Err(err) => error!(id = self.get_id(), "Error: {err}"),
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
mod message {
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Action {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
on: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
scene: Option<String>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn on(on: bool) -> Self {
|
||||
Self {
|
||||
on: Some(on),
|
||||
scene: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scene(scene: String) -> Self {
|
||||
Self {
|
||||
on: None,
|
||||
scene: Some(scene),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct State {
|
||||
all_on: bool,
|
||||
any_on: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Info {
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn any_on(&self) -> bool {
|
||||
self.state.any_on
|
||||
}
|
||||
|
||||
// pub fn all_on(&self) -> bool {
|
||||
// self.state.all_on
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Timeout {
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Timeout {
|
||||
pub fn new(timeout: Option<Duration>) -> Self {
|
||||
Self { timeout }
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Timeout {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let len = if self.timeout.is_some() { 2 } else { 1 };
|
||||
let mut state = serializer.serialize_struct("TimerMessage", len)?;
|
||||
if self.timeout.is_some() {
|
||||
state.serialize_field("status", "enabled")?;
|
||||
} else {
|
||||
state.serialize_field("status", "disabled")?;
|
||||
}
|
||||
|
||||
if let Some(timeout) = self.timeout {
|
||||
let seconds = timeout.as_secs() % 60;
|
||||
let minutes = (timeout.as_secs() / 60) % 60;
|
||||
let hours = timeout.as_secs() / 3600;
|
||||
|
||||
let time = format!("PT{hours:<02}:{minutes:<02}:{seconds:<02}");
|
||||
state.serialize_field("localtime", &time)?;
|
||||
};
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use axum::async_trait;
|
||||
use rumqttc::{matches, Publish};
|
||||
use tracing::{debug, trace, warn};
|
||||
use zigbee2mqtt_types::vendors::philips::Zigbee929003017102;
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::action_callback::ActionCallback;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub left_callback: ActionCallback<()>,
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub right_callback: ActionCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HueSwitch {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Device for HueSwitch {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for HueSwitch {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.info.identifier(), "Setting up HueSwitch");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for HueSwitch {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the deviec itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
let action = match serde_json::from_slice::<Zigbee929003017102>(&message.payload) {
|
||||
Ok(message) => message.action,
|
||||
Err(err) => {
|
||||
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||
|
||||
match action {
|
||||
zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Leftpress => {
|
||||
self.config.left_callback.call(()).await
|
||||
}
|
||||
zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Rightpress => {
|
||||
self.config.right_callback.call(()).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::device;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{self, OnOff};
|
||||
use google_home::types::Type;
|
||||
use rumqttc::{matches, Publish};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::action_callback::ActionCallback;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::OnOffMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
|
||||
pub enum OutletType {
|
||||
Outlet,
|
||||
Kettle,
|
||||
Charger,
|
||||
Light,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(default(OutletType::Outlet))]
|
||||
pub outlet_type: OutletType,
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub callback: ActionCallback<(IkeaOutlet, bool)>,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State {
|
||||
last_known_state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IkeaOutlet {
|
||||
config: Config,
|
||||
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl IkeaOutlet {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for IkeaOutlet {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
state: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for IkeaOutlet {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for IkeaOutlet {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the deviec itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
// Update the internal state based on what the device has reported
|
||||
let state = match OnOffMessage::try_from(message) {
|
||||
Ok(state) => state.state(),
|
||||
Err(err) => {
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// No need to do anything if the state has not changed
|
||||
if state == self.state().await.last_known_state {
|
||||
return;
|
||||
}
|
||||
|
||||
self.config.callback.call((self.clone(), state)).await;
|
||||
|
||||
debug!(id = Device::get_id(self), "Updating state to {state}");
|
||||
self.state_mut().await.last_known_state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for IkeaOutlet {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
// Turn off the outlet when we leave the house (Not if it is a battery charger)
|
||||
if !presence && self.config.outlet_type != OutletType::Charger {
|
||||
debug!(id = Device::get_id(self), "Turning device off");
|
||||
self.set_on(false).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl google_home::Device for IkeaOutlet {
|
||||
fn get_device_type(&self) -> Type {
|
||||
match self.config.outlet_type {
|
||||
OutletType::Outlet => Type::Outlet,
|
||||
OutletType::Kettle => Type::Kettle,
|
||||
OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work
|
||||
OutletType::Charger => Type::Outlet, // Find a better device type for this, ideally would like to use charger, but that needs more work
|
||||
}
|
||||
}
|
||||
|
||||
fn get_device_name(&self) -> device::Name {
|
||||
device::Name::new(&self.config.info.name)
|
||||
}
|
||||
|
||||
fn get_id(&self) -> String {
|
||||
Device::get_id(self)
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_room_hint(&self) -> Option<&str> {
|
||||
self.config.info.room.as_deref()
|
||||
}
|
||||
|
||||
fn will_report_state(&self) -> bool {
|
||||
// TODO: Implement state reporting
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl traits::OnOff for IkeaOutlet {
|
||||
async fn on(&self) -> Result<bool, ErrorCode> {
|
||||
Ok(self.state().await.last_known_state)
|
||||
}
|
||||
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
let message = OnOffMessage::new(on);
|
||||
|
||||
let topic = format!("{}/set", self.config.mqtt.topic);
|
||||
// TODO: Handle potential errors here
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use axum::async_trait;
|
||||
use rumqttc::{matches, Publish};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::action_callback::ActionCallback;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::RemoteMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
|
||||
#[device_config(default)]
|
||||
pub single_button: bool,
|
||||
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub callback: ActionCallback<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IkeaRemote {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Device for IkeaRemote {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for IkeaRemote {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.info.identifier(), "Setting up IkeaRemote");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for IkeaRemote {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the deviec itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||
|
||||
let on = if self.config.single_button {
|
||||
match action {
|
||||
crate::messages::RemoteAction::On => Some(true),
|
||||
crate::messages::RemoteAction::BrightnessMoveUp => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match action {
|
||||
crate::messages::RemoteAction::On => Some(true),
|
||||
crate::messages::RemoteAction::Off => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(on) = on {
|
||||
self.config.callback.call(on).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use bytes::{Buf, BufMut};
|
||||
use google_home::errors::{self, DeviceError};
|
||||
use google_home::traits::OnOff;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::event::OnPresence;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KasaOutlet {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for KasaOutlet {
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up KasaOutlet");
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for KasaOutlet {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RequestRelayState {
|
||||
state: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RequestSysinfo;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RequestSystem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
get_sysinfo: Option<RequestSysinfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
set_relay_state: Option<RequestRelayState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Request {
|
||||
system: RequestSystem,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
fn get_sysinfo() -> Self {
|
||||
Self {
|
||||
system: RequestSystem {
|
||||
get_sysinfo: Some(RequestSysinfo {}),
|
||||
set_relay_state: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_relay_state(on: bool) -> Self {
|
||||
Self {
|
||||
system: RequestSystem {
|
||||
get_sysinfo: None,
|
||||
set_relay_state: Some(RequestRelayState {
|
||||
state: if on { 1 } else { 0 },
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt(&self) -> bytes::Bytes {
|
||||
let data: bytes::Bytes = serde_json::to_string(self).unwrap().into();
|
||||
|
||||
let mut key: u8 = 171;
|
||||
let mut encrypted = bytes::BytesMut::with_capacity(data.len() + 4);
|
||||
|
||||
encrypted.put_u32(data.len() as u32);
|
||||
|
||||
for c in data {
|
||||
key ^= c;
|
||||
encrypted.put_u8(key);
|
||||
}
|
||||
|
||||
encrypted.freeze()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ErrorCode {
|
||||
err_code: isize,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
fn ok(&self) -> Result<(), ResponseError> {
|
||||
if self.err_code != 0 {
|
||||
Err(ResponseError::ErrorCode(self.err_code))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseSetRelayState {
|
||||
#[serde(flatten)]
|
||||
err_code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseGetSysinfo {
|
||||
#[serde(flatten)]
|
||||
err_code: ErrorCode,
|
||||
relay_state: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseSystem {
|
||||
set_relay_state: Option<ResponseSetRelayState>,
|
||||
get_sysinfo: Option<ResponseGetSysinfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Response {
|
||||
system: ResponseSystem,
|
||||
}
|
||||
|
||||
// TODO: Improve this error
|
||||
#[derive(Debug, Error)]
|
||||
enum ResponseError {
|
||||
#[error("Expected a minimum data length of 4")]
|
||||
ToShort,
|
||||
#[error("No sysinfo found in response")]
|
||||
SysinfoNotFound,
|
||||
#[error("No relay_state not found in response")]
|
||||
RelayStateNotFound,
|
||||
#[error("Error code: {0}")]
|
||||
ErrorCode(isize),
|
||||
#[error(transparent)]
|
||||
Other(#[from] Box<dyn std::error::Error>),
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for ResponseError {
|
||||
fn from(err: Utf8Error) -> Self {
|
||||
ResponseError::Other(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ResponseError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ResponseError::Other(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn get_current_relay_state(&self) -> Result<bool, ResponseError> {
|
||||
if let Some(sysinfo) = &self.system.get_sysinfo {
|
||||
return sysinfo.err_code.ok().map(|_| sysinfo.relay_state == 1);
|
||||
}
|
||||
|
||||
Err(ResponseError::SysinfoNotFound)
|
||||
}
|
||||
|
||||
fn check_set_relay_success(&self) -> Result<(), ResponseError> {
|
||||
if let Some(set_relay_state) = &self.system.set_relay_state {
|
||||
return set_relay_state.err_code.ok();
|
||||
}
|
||||
|
||||
Err(ResponseError::RelayStateNotFound)
|
||||
}
|
||||
|
||||
fn decrypt(mut data: bytes::Bytes) -> Result<Self, ResponseError> {
|
||||
let mut key: u8 = 171;
|
||||
if data.len() < 4 {
|
||||
return Err(ResponseError::ToShort);
|
||||
}
|
||||
|
||||
let length = data.get_u32();
|
||||
let mut decrypted = bytes::BytesMut::with_capacity(length as usize);
|
||||
|
||||
for c in data {
|
||||
decrypted.put_u8(key ^ c);
|
||||
key = c;
|
||||
}
|
||||
|
||||
let decrypted = std::str::from_utf8(&decrypted)?;
|
||||
Ok(serde_json::from_str(decrypted)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnOff for KasaOutlet {
|
||||
async fn on(&self) -> Result<bool, errors::ErrorCode> {
|
||||
let mut stream = TcpStream::connect(self.config.addr)
|
||||
.await
|
||||
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
||||
let body = Request::get_sysinfo().encrypt();
|
||||
stream
|
||||
.write_all(&body)
|
||||
.await
|
||||
.and(stream.flush().await)
|
||||
.or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
|
||||
let mut received = Vec::new();
|
||||
let mut rx_bytes = [0; 1024];
|
||||
loop {
|
||||
let read = stream
|
||||
.read(&mut rx_bytes)
|
||||
.await
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
received.extend_from_slice(&rx_bytes[..read]);
|
||||
|
||||
if read < rx_bytes.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let resp = Response::decrypt(received.into())
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
resp.get_current_relay_state()
|
||||
.or(Err(DeviceError::TransientError.into()))
|
||||
}
|
||||
|
||||
async fn set_on(&self, on: bool) -> Result<(), errors::ErrorCode> {
|
||||
let mut stream = TcpStream::connect(self.config.addr)
|
||||
.await
|
||||
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
||||
let body = Request::set_relay_state(on).encrypt();
|
||||
stream
|
||||
.write_all(&body)
|
||||
.await
|
||||
.and(stream.flush().await)
|
||||
.or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
|
||||
let mut received = Vec::new();
|
||||
let mut rx_bytes = [0; 1024];
|
||||
loop {
|
||||
let read = match stream.read(&mut rx_bytes).await {
|
||||
Ok(read) => read,
|
||||
Err(_) => return Err(DeviceError::TransientError.into()),
|
||||
};
|
||||
|
||||
received.extend_from_slice(&rx_bytes[..read]);
|
||||
|
||||
if read < rx_bytes.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let resp = Response::decrypt(received.into())
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
resp.check_set_relay_success()
|
||||
.or(Err(DeviceError::TransientError.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for KasaOutlet {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
if !presence {
|
||||
debug!(id = Device::get_id(self), "Turning device off");
|
||||
self.set_on(false).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::BrightnessMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
pub min: isize,
|
||||
pub max: isize,
|
||||
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
const DEFAULT: bool = false;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
is_dark: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LightSensor {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl LightSensor {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for LightSensor {
|
||||
type Config = Config;
|
||||
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?;
|
||||
|
||||
let state = State { is_dark: DEFAULT };
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for LightSensor {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for LightSensor {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let illuminance = match BrightnessMessage::try_from(message) {
|
||||
Ok(state) => state.illuminance(),
|
||||
Err(err) => {
|
||||
warn!("Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Illuminance: {illuminance}");
|
||||
let is_dark = if illuminance <= self.config.min {
|
||||
trace!("It is dark");
|
||||
true
|
||||
} else if illuminance >= self.config.max {
|
||||
trace!("It is light");
|
||||
false
|
||||
} else {
|
||||
let is_dark = self.state().await.is_dark;
|
||||
trace!(
|
||||
"In between min ({}) and max ({}) value, keeping current state: {}",
|
||||
self.config.min,
|
||||
self.config.max,
|
||||
is_dark
|
||||
);
|
||||
is_dark
|
||||
};
|
||||
|
||||
if is_dark != self.state().await.is_dark {
|
||||
debug!("Dark state has changed: {is_dark}");
|
||||
self.state_mut().await.is_dark = is_dark;
|
||||
|
||||
if self.config.tx.send(Event::Darkness(is_dark)).await.is_err() {
|
||||
warn!("There are no receivers on the event channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
mod air_filter;
|
||||
mod contact_sensor;
|
||||
mod debug_bridge;
|
||||
mod hue_bridge;
|
||||
mod hue_group;
|
||||
mod hue_switch;
|
||||
mod ikea_outlet;
|
||||
mod ikea_remote;
|
||||
mod kasa_outlet;
|
||||
mod light_sensor;
|
||||
mod ntfy;
|
||||
mod presence;
|
||||
mod wake_on_lan;
|
||||
mod washer;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_cast::Cast;
|
||||
use dyn_clone::DynClone;
|
||||
use google_home::traits::OnOff;
|
||||
use mlua::ObjectLike;
|
||||
|
||||
pub use self::air_filter::AirFilter;
|
||||
pub use self::contact_sensor::ContactSensor;
|
||||
pub use self::debug_bridge::DebugBridge;
|
||||
pub use self::hue_bridge::HueBridge;
|
||||
pub use self::hue_group::HueGroup;
|
||||
pub use self::hue_switch::HueSwitch;
|
||||
pub use self::ikea_outlet::IkeaOutlet;
|
||||
pub use self::ikea_remote::IkeaRemote;
|
||||
pub use self::kasa_outlet::KasaOutlet;
|
||||
pub use self::light_sensor::LightSensor;
|
||||
pub use self::ntfy::{Notification, Ntfy};
|
||||
pub use self::presence::{Presence, DEFAULT_PRESENCE};
|
||||
pub use self::wake_on_lan::WakeOnLAN;
|
||||
pub use self::washer::Washer;
|
||||
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
|
||||
#[async_trait]
|
||||
pub trait LuaDeviceCreate {
|
||||
type Config;
|
||||
type Error;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
macro_rules! register_device {
|
||||
($lua:expr, $device:ty) => {
|
||||
$lua.globals()
|
||||
.set(stringify!($device), $lua.create_proxy::<$device>()?)?;
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_device {
|
||||
($device:ty) => {
|
||||
impl mlua::UserData for $device {
|
||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_async_function("new", |_lua, config| async {
|
||||
let device: $device = crate::devices::LuaDeviceCreate::create(config)
|
||||
.await
|
||||
.map_err(mlua::ExternalError::into_lua_err)?;
|
||||
|
||||
Ok(device)
|
||||
});
|
||||
|
||||
methods.add_method("__box", |_lua, this, _: ()| {
|
||||
let b: Box<dyn Device> = Box::new(this.clone());
|
||||
Ok(b)
|
||||
});
|
||||
|
||||
methods.add_async_method("get_id", |_lua, this, _: ()| async move { Ok(this.get_id()) });
|
||||
|
||||
if impls::impls!($device: OnOff) {
|
||||
methods.add_async_method("set_on", |_lua, this, on: bool| async move {
|
||||
(this.deref().cast() as Option<&dyn OnOff>)
|
||||
.expect("Cast should be valid")
|
||||
.set_on(on)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
methods.add_async_method("is_on", |_lua, this, _: ()| async move {
|
||||
Ok((this.deref().cast() as Option<&dyn OnOff>)
|
||||
.expect("Cast should be valid")
|
||||
.on()
|
||||
.await
|
||||
.unwrap())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_device!(AirFilter);
|
||||
impl_device!(ContactSensor);
|
||||
impl_device!(DebugBridge);
|
||||
impl_device!(HueBridge);
|
||||
impl_device!(HueGroup);
|
||||
impl_device!(HueSwitch);
|
||||
impl_device!(IkeaOutlet);
|
||||
impl_device!(IkeaRemote);
|
||||
impl_device!(KasaOutlet);
|
||||
impl_device!(LightSensor);
|
||||
impl_device!(Ntfy);
|
||||
impl_device!(Presence);
|
||||
impl_device!(WakeOnLAN);
|
||||
impl_device!(Washer);
|
||||
|
||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
register_device!(lua, AirFilter);
|
||||
register_device!(lua, ContactSensor);
|
||||
register_device!(lua, DebugBridge);
|
||||
register_device!(lua, HueBridge);
|
||||
register_device!(lua, HueGroup);
|
||||
register_device!(lua, HueSwitch);
|
||||
register_device!(lua, IkeaOutlet);
|
||||
register_device!(lua, IkeaRemote);
|
||||
register_device!(lua, KasaOutlet);
|
||||
register_device!(lua, LightSensor);
|
||||
register_device!(lua, Ntfy);
|
||||
register_device!(lua, Presence);
|
||||
register_device!(lua, WakeOnLAN);
|
||||
register_device!(lua, Washer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait Device:
|
||||
Debug
|
||||
+ DynClone
|
||||
+ Sync
|
||||
+ Send
|
||||
+ Cast<dyn google_home::Device>
|
||||
+ Cast<dyn OnMqtt>
|
||||
+ Cast<dyn OnPresence>
|
||||
+ Cast<dyn OnDarkness>
|
||||
+ Cast<dyn OnNotification>
|
||||
+ Cast<dyn OnOff>
|
||||
{
|
||||
fn get_id(&self) -> String;
|
||||
}
|
||||
|
||||
impl mlua::FromLua for Box<dyn Device> {
|
||||
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
|
||||
match value {
|
||||
mlua::Value::UserData(ud) => {
|
||||
let ud = if ud.is::<Box<dyn Device>>() {
|
||||
ud
|
||||
} else {
|
||||
ud.call_method::<_>("__box", ())?
|
||||
};
|
||||
|
||||
let b = ud.borrow::<Self>()?.clone();
|
||||
Ok(b)
|
||||
}
|
||||
_ => Err(mlua::Error::RuntimeError("Expected user data".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl mlua::UserData for Box<dyn Device> {}
|
||||
|
||||
dyn_clone::clone_trait_object!(Device);
|
||||
@@ -1,208 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use serde::Serialize;
|
||||
use serde_repr::*;
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
|
||||
|
||||
#[derive(Debug, Serialize_repr, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum Priority {
|
||||
Min = 1,
|
||||
Low,
|
||||
Default,
|
||||
High,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "snake_case", tag = "action")]
|
||||
pub enum ActionType {
|
||||
Broadcast {
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
extras: HashMap<String, String>,
|
||||
},
|
||||
// View,
|
||||
// Http
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Action {
|
||||
#[serde(flatten)]
|
||||
pub action: ActionType,
|
||||
pub label: String,
|
||||
pub clear: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NotificationFinal {
|
||||
topic: String,
|
||||
#[serde(flatten)]
|
||||
inner: Notification,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Notification {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
tags: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
priority: Option<Priority>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: None,
|
||||
message: None,
|
||||
tags: Vec::new(),
|
||||
priority: None,
|
||||
actions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title(mut self, title: &str) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_message(mut self, message: &str) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_tag(mut self, tag: &str) -> Self {
|
||||
self.tags.push(tag.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_priority(mut self, priority: Priority) -> Self {
|
||||
self.priority = Some(priority);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_action(mut self, action: Action) -> Self {
|
||||
self.actions.push(action);
|
||||
self
|
||||
}
|
||||
|
||||
fn finalize(self, topic: &str) -> NotificationFinal {
|
||||
NotificationFinal {
|
||||
topic: topic.into(),
|
||||
inner: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Notification {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(default("https://ntfy.sh".into()))]
|
||||
pub url: String,
|
||||
pub topic: String,
|
||||
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ntfy {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Ntfy {
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = "ntfy", "Setting up Ntfy");
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Ntfy {
|
||||
fn get_id(&self) -> String {
|
||||
"ntfy".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ntfy {
|
||||
async fn send(&self, notification: Notification) {
|
||||
let notification = notification.finalize(&self.config.topic);
|
||||
|
||||
// Create the request
|
||||
let res = reqwest::Client::new()
|
||||
.post(self.config.url.clone())
|
||||
.json(¬ification)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("Something went wrong while sending the notification: {err}");
|
||||
} else if let Ok(res) = res {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!("Received status {status} when sending notification");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for Ntfy {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
// Setup extras for the broadcast
|
||||
let extras = HashMap::from([
|
||||
("cmd".into(), "presence".into()),
|
||||
("state".into(), if presence { "0" } else { "1" }.into()),
|
||||
]);
|
||||
|
||||
// Create broadcast action
|
||||
let action = Action {
|
||||
action: ActionType::Broadcast { extras },
|
||||
label: if presence { "Set away" } else { "Set home" }.into(),
|
||||
clear: Some(true),
|
||||
};
|
||||
|
||||
// Create the notification
|
||||
let notification = Notification::new()
|
||||
.set_title("Presence")
|
||||
.set_message(if presence { "Home" } else { "Away" })
|
||||
.add_tag("house")
|
||||
.add_action(action)
|
||||
.set_priority(Priority::Low);
|
||||
|
||||
if self
|
||||
.config
|
||||
.tx
|
||||
.send(Event::Ntfy(notification))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!("There are no receivers on the event channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnNotification for Ntfy {
|
||||
async fn on_notification(&self, notification: Notification) {
|
||||
self.send(notification).await;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::PresenceMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua, rename("event_channel"), with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
pub const DEFAULT_PRESENCE: bool = false;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
devices: HashMap<String, bool>,
|
||||
current_overall_presence: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Presence {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl Presence {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Presence {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = "presence", "Setting up Presence");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
let state = State {
|
||||
devices: HashMap::new(),
|
||||
current_overall_presence: DEFAULT_PRESENCE,
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Presence {
|
||||
fn get_id(&self) -> String {
|
||||
"presence".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Presence {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = self
|
||||
.config
|
||||
.mqtt
|
||||
.topic
|
||||
.find('+')
|
||||
.or(self.config.mqtt.topic.find('#'))
|
||||
.expect("Presence::create fails if it does not contain wildcards");
|
||||
let device_name = message.topic[offset..].into();
|
||||
|
||||
if message.payload.is_empty() {
|
||||
// Remove the device from the map
|
||||
debug!("State of device [{device_name}] has been removed");
|
||||
self.state_mut().await.devices.remove(&device_name);
|
||||
} else {
|
||||
let present = match PresenceMessage::try_from(message) {
|
||||
Ok(state) => state.presence(),
|
||||
Err(err) => {
|
||||
warn!("Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("State of device [{device_name}] has changed: {}", present);
|
||||
self.state_mut().await.devices.insert(device_name, present);
|
||||
}
|
||||
|
||||
let overall_presence = self.state().await.devices.iter().any(|(_, v)| *v);
|
||||
if overall_presence != self.state().await.current_overall_presence {
|
||||
debug!("Overall presence updated: {overall_presence}");
|
||||
self.state_mut().await.current_overall_presence = overall_presence;
|
||||
|
||||
if self
|
||||
.config
|
||||
.tx
|
||||
.send(Event::Presence(overall_presence))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!("There are no receivers on the event channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use eui48::MacAddress;
|
||||
use google_home::device;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{self, Scene};
|
||||
use google_home::types::Type;
|
||||
use rumqttc::Publish;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::ActivateMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
pub mac_address: MacAddress,
|
||||
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
|
||||
pub broadcast_ip: Ipv4Addr,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WakeOnLAN {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for WakeOnLAN {
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.info.identifier(), "Setting up WakeOnLAN");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for WakeOnLAN {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for WakeOnLAN {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let activate = match ActivateMessage::try_from(message) {
|
||||
Ok(message) => message.activate(),
|
||||
Err(err) => {
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.set_active(activate).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl google_home::Device for WakeOnLAN {
|
||||
fn get_device_type(&self) -> Type {
|
||||
Type::Scene
|
||||
}
|
||||
|
||||
fn get_device_name(&self) -> device::Name {
|
||||
let mut name = device::Name::new(&self.config.info.name);
|
||||
name.add_default_name("Computer");
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
fn get_id(&self) -> String {
|
||||
Device::get_id(self)
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_room_hint(&self) -> Option<&str> {
|
||||
self.config.info.room.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl traits::Scene for WakeOnLAN {
|
||||
async fn set_active(&self, deactivate: bool) -> Result<(), ErrorCode> {
|
||||
if deactivate {
|
||||
debug!(
|
||||
id = Device::get_id(self),
|
||||
"Trying to deactivate computer, this is not currently supported"
|
||||
);
|
||||
// We do not support deactivating this scene
|
||||
Err(ErrorCode::DeviceError(
|
||||
google_home::errors::DeviceError::ActionNotAvailable,
|
||||
))
|
||||
} else {
|
||||
debug!(
|
||||
id = Device::get_id(self),
|
||||
"Activating Computer: {} (Sending to {})",
|
||||
self.config.mac_address,
|
||||
self.config.broadcast_ip
|
||||
);
|
||||
let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
|
||||
|err| {
|
||||
error!(id = Device::get_id(self), "invalid mac address: {err}");
|
||||
google_home::errors::DeviceError::TransientError
|
||||
},
|
||||
)?;
|
||||
|
||||
wol.send_magic_to(
|
||||
(Ipv4Addr::new(0, 0, 0, 0), 0),
|
||||
(self.config.broadcast_ip, 9),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
id = Device::get_id(self),
|
||||
"Failed to activate computer: {err}"
|
||||
);
|
||||
google_home::errors::DeviceError::TransientError.into()
|
||||
})
|
||||
.map(|_| debug!(id = Device::get_id(self), "Success!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::ntfy::Priority;
|
||||
use super::{Device, LuaDeviceCreate, Notification};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::PowerMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
// Power in Watt
|
||||
pub threshold: f32,
|
||||
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
running: isize,
|
||||
}
|
||||
|
||||
// TODO: Add google home integration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Washer {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl Washer {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Washer {
|
||||
type Config = Config;
|
||||
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?;
|
||||
|
||||
let state = State { running: 0 };
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Washer {
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// The washer needs to have a power draw above the threshold multiple times before the washer is
|
||||
// actually marked as running
|
||||
// This helps prevent false positives
|
||||
const HYSTERESIS: isize = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Washer {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let power = match PowerMessage::try_from(message) {
|
||||
Ok(state) => state.power(),
|
||||
Err(err) => {
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// debug!(id = self.identifier, power, "Washer state update");
|
||||
|
||||
if power < self.config.threshold && self.state().await.running >= HYSTERESIS {
|
||||
// The washer is done running
|
||||
debug!(
|
||||
id = self.config.identifier,
|
||||
power,
|
||||
threshold = self.config.threshold,
|
||||
"Washer is done"
|
||||
);
|
||||
|
||||
self.state_mut().await.running = 0;
|
||||
let notification = Notification::new()
|
||||
.set_title("Laundy is done")
|
||||
.set_message("Don't forget to hang it!")
|
||||
.add_tag("womans_clothes")
|
||||
.set_priority(Priority::High);
|
||||
|
||||
if self
|
||||
.config
|
||||
.tx
|
||||
.send(Event::Ntfy(notification))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!("There are no receivers on the event channel");
|
||||
}
|
||||
} else if power < self.config.threshold {
|
||||
// Prevent false positives
|
||||
self.state_mut().await.running = 0;
|
||||
} else if power >= self.config.threshold && self.state().await.running < HYSTERESIS {
|
||||
// Washer could be starting
|
||||
debug!(
|
||||
id = self.config.identifier,
|
||||
power,
|
||||
threshold = self.config.threshold,
|
||||
"Washer is starting"
|
||||
);
|
||||
|
||||
self.state_mut().await.running += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/error.rs
168
src/error.rs
@@ -1,168 +0,0 @@
|
||||
use std::{error, fmt, result};
|
||||
|
||||
use axum::http::status::InvalidStatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use bytes::Bytes;
|
||||
use rumqttc::ClientError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MissingEnv {
|
||||
keys: Vec<String>,
|
||||
}
|
||||
|
||||
// TODO: Would be nice to somehow get the line number of the missing keys
|
||||
impl MissingEnv {
|
||||
pub fn new() -> Self {
|
||||
Self { keys: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_missing(&mut self, key: &str) {
|
||||
self.keys.push(key.into());
|
||||
}
|
||||
|
||||
pub fn has_missing(self) -> result::Result<(), Self> {
|
||||
if !self.keys.is_empty() {
|
||||
Err(self)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MissingEnv {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MissingEnv {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Missing environment variable")?;
|
||||
if self.keys.is_empty() {
|
||||
unreachable!("This error should only be returned if there are actually missing environment variables");
|
||||
}
|
||||
if self.keys.len() == 1 {
|
||||
write!(f, " '{}'", self.keys[0])?;
|
||||
} else {
|
||||
write!(f, "s '{}'", self.keys[0])?;
|
||||
self.keys
|
||||
.iter()
|
||||
.skip(1)
|
||||
.try_for_each(|key| write!(f, ", '{key}'"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for MissingEnv {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseError {
|
||||
#[error("Invalid message payload received: {0:?}")]
|
||||
InvalidPayload(Bytes),
|
||||
}
|
||||
|
||||
// TODO: Would be nice to somehow get the line number of the expected wildcard topic
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Topic '{topic}' is expected to be a wildcard topic")]
|
||||
pub struct MissingWildcard {
|
||||
topic: String,
|
||||
}
|
||||
|
||||
impl MissingWildcard {
|
||||
pub fn new(topic: &str) -> Self {
|
||||
Self {
|
||||
topic: topic.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeviceConfigError {
|
||||
#[error("Device '{0}' does not implement expected trait '{1}'")]
|
||||
MissingTrait(String, String),
|
||||
#[error(transparent)]
|
||||
MqttClientError(#[from] rumqttc::ClientError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PresenceError {
|
||||
#[error(transparent)]
|
||||
SubscribeError(#[from] ClientError),
|
||||
#[error(transparent)]
|
||||
MissingWildcard(#[from] MissingWildcard),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LightSensorError {
|
||||
#[error(transparent)]
|
||||
SubscribeError(#[from] ClientError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{source}")]
|
||||
pub struct ApiError {
|
||||
status_code: axum::http::StatusCode,
|
||||
source: Box<dyn std::error::Error>,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn new(status_code: axum::http::StatusCode, source: Box<dyn std::error::Error>) -> Self {
|
||||
Self {
|
||||
status_code,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for ApiErrorJson {
|
||||
fn from(value: ApiError) -> Self {
|
||||
let error = ApiErrorJsonError {
|
||||
code: value.status_code.as_u16(),
|
||||
status: value.status_code.to_string(),
|
||||
reason: value.source.to_string(),
|
||||
};
|
||||
|
||||
Self { error }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
self.status_code,
|
||||
serde_json::to_string::<ApiErrorJson>(&self.into())
|
||||
.expect("Serialization should not fail"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ApiErrorJsonError {
|
||||
code: u16,
|
||||
status: String,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiErrorJson {
|
||||
error: ApiErrorJsonError,
|
||||
}
|
||||
|
||||
impl TryFrom<ApiErrorJson> for ApiError {
|
||||
type Error = InvalidStatusCode;
|
||||
|
||||
fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> {
|
||||
let status_code = axum::http::StatusCode::from_u16(value.error.code)?;
|
||||
let source = value.error.reason.into();
|
||||
|
||||
Ok(Self {
|
||||
status_code,
|
||||
source,
|
||||
})
|
||||
}
|
||||
}
|
||||
55
src/event.rs
55
src/event.rs
@@ -1,55 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use mlua::FromLua;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::devices::Notification;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
MqttMessage(Publish),
|
||||
Darkness(bool),
|
||||
Presence(bool),
|
||||
Ntfy(Notification),
|
||||
}
|
||||
|
||||
pub type Sender = mpsc::Sender<Event>;
|
||||
pub type Receiver = mpsc::Receiver<Event>;
|
||||
|
||||
#[derive(Clone, Debug, FromLua)]
|
||||
pub struct EventChannel(Sender);
|
||||
|
||||
impl EventChannel {
|
||||
pub fn new() -> (Self, Receiver) {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
|
||||
(Self(tx), rx)
|
||||
}
|
||||
|
||||
pub fn get_tx(&self) -> Sender {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl mlua::UserData for EventChannel {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnMqtt: Sync + Send {
|
||||
// fn topics(&self) -> Vec<&str>;
|
||||
async fn on_mqtt(&self, message: Publish);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnPresence: Sync + Send {
|
||||
async fn on_presence(&self, presence: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnDarkness: Sync + Send {
|
||||
async fn on_darkness(&self, dark: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnNotification: Sync + Send {
|
||||
async fn on_notification(&self, notification: Notification);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
mod timeout;
|
||||
|
||||
pub use timeout::Timeout;
|
||||
|
||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
lua.globals()
|
||||
.set("Timeout", lua.create_proxy::<Timeout>()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::action_callback::ActionCallback;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State {
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timeout {
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl mlua::UserData for Timeout {
|
||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_function("new", |_lua, ()| {
|
||||
let device = Self {
|
||||
state: Default::default(),
|
||||
};
|
||||
|
||||
Ok(device)
|
||||
});
|
||||
|
||||
methods.add_async_method(
|
||||
"start",
|
||||
|_lua, this, (timeout, callback): (u64, ActionCallback<bool>)| async move {
|
||||
if let Some(handle) = this.state.write().await.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
debug!("Running timeout callback after {timeout}s");
|
||||
|
||||
let timeout = Duration::from_secs(timeout);
|
||||
|
||||
this.state.write().await.handle = Some(tokio::spawn({
|
||||
async move {
|
||||
tokio::time::sleep(timeout).await;
|
||||
|
||||
callback.call(false).await;
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_method("cancel", |_lua, this, ()| async move {
|
||||
debug!("Canceling timeout callback");
|
||||
|
||||
if let Some(handle) = this.state.write().await.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
methods.add_async_method("is_waiting", |_lua, this, ()| async move {
|
||||
debug!("Canceling timeout callback");
|
||||
|
||||
if let Some(handle) = this.state.read().await.handle.as_ref() {
|
||||
debug!("Join handle: {}", handle.is_finished());
|
||||
return Ok(!handle.is_finished());
|
||||
}
|
||||
|
||||
debug!("Join handle: None");
|
||||
|
||||
Ok(false)
|
||||
});
|
||||
}
|
||||
}
|
||||
15
src/lib.rs
15
src/lib.rs
@@ -1,15 +0,0 @@
|
||||
#![allow(incomplete_features)]
|
||||
#![feature(specialization)]
|
||||
#![feature(let_chains)]
|
||||
|
||||
pub mod action_callback;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod device_manager;
|
||||
pub mod devices;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod helpers;
|
||||
pub mod messages;
|
||||
pub mod mqtt;
|
||||
pub mod schedule;
|
||||
20
src/main.rs
20
src/main.rs
@@ -1,13 +1,15 @@
|
||||
mod web;
|
||||
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use automation::auth::User;
|
||||
use automation::config::{FulfillmentConfig, MqttConfig};
|
||||
use automation::device_manager::DeviceManager;
|
||||
use automation::error::ApiError;
|
||||
use automation::mqtt::{self, WrappedAsyncClient};
|
||||
use automation::{devices, helpers};
|
||||
use automation_lib::config::{FulfillmentConfig, MqttConfig};
|
||||
use automation_lib::device_manager::DeviceManager;
|
||||
use automation_lib::helpers;
|
||||
use automation_lib::mqtt::{self, WrappedAsyncClient};
|
||||
use automation_lib::ntfy::Ntfy;
|
||||
use automation_lib::presence::Presence;
|
||||
use axum::extract::{FromRef, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::post;
|
||||
@@ -17,6 +19,7 @@ use google_home::{GoogleHome, Request, Response};
|
||||
use mlua::LuaSerdeExt;
|
||||
use rumqttc::AsyncClient;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use web::{ApiError, User};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
@@ -111,8 +114,11 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
lua.globals().set("automation", automation)?;
|
||||
|
||||
devices::register_with_lua(&lua)?;
|
||||
automation_devices::register_with_lua(&lua)?;
|
||||
helpers::register_with_lua(&lua)?;
|
||||
lua.globals().set("Ntfy", lua.create_proxy::<Ntfy>()?)?;
|
||||
lua.globals()
|
||||
.set("Presence", lua.create_proxy::<Presence>()?)?;
|
||||
|
||||
// TODO: Make this not hardcoded
|
||||
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());
|
||||
|
||||
280
src/messages.rs
280
src/messages.rs
@@ -1,280 +0,0 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use bytes::Bytes;
|
||||
use rumqttc::Publish;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::ParseError;
|
||||
|
||||
// Message used to turn on and off devices and receiving their state
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OnOffMessage {
|
||||
state: String,
|
||||
}
|
||||
|
||||
impl OnOffMessage {
|
||||
pub fn new(state: bool) -> Self {
|
||||
Self {
|
||||
state: if state { "ON" } else { "OFF" }.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> bool {
|
||||
self.state == "ON"
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for OnOffMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message send to request activating a device
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ActivateMessage {
|
||||
activate: bool,
|
||||
}
|
||||
|
||||
impl ActivateMessage {
|
||||
pub fn activate(&self) -> bool {
|
||||
self.activate
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for ActivateMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Actions that can be performed by a remote
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RemoteAction {
|
||||
On,
|
||||
Off,
|
||||
BrightnessMoveUp,
|
||||
BrightnessMoveDown,
|
||||
BrightnessStop,
|
||||
}
|
||||
|
||||
// Message used to report the action performed by a remote
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RemoteMessage {
|
||||
action: RemoteAction,
|
||||
}
|
||||
|
||||
impl RemoteMessage {
|
||||
pub fn action(&self) -> RemoteAction {
|
||||
self.action
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for RemoteMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message used to report the current presence state
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PresenceMessage {
|
||||
state: bool,
|
||||
updated: Option<u128>,
|
||||
}
|
||||
|
||||
impl PresenceMessage {
|
||||
pub fn new(state: bool) -> Self {
|
||||
Self {
|
||||
state,
|
||||
updated: Some(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time is after UNIX EPOCH")
|
||||
.as_millis(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn presence(&self) -> bool {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for PresenceMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message used to report the state of a light sensor
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BrightnessMessage {
|
||||
illuminance: isize,
|
||||
}
|
||||
|
||||
impl BrightnessMessage {
|
||||
pub fn illuminance(&self) -> isize {
|
||||
self.illuminance
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for BrightnessMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message to report the state of a contact sensor
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ContactMessage {
|
||||
contact: bool,
|
||||
}
|
||||
|
||||
impl ContactMessage {
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.contact
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for ContactMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message used to report the current darkness state
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct DarknessMessage {
|
||||
state: bool,
|
||||
updated: Option<u128>,
|
||||
}
|
||||
|
||||
impl DarknessMessage {
|
||||
pub fn new(state: bool) -> Self {
|
||||
Self {
|
||||
state,
|
||||
updated: Some(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time is after UNIX EPOCH")
|
||||
.as_millis(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dark(&self) -> bool {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for DarknessMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message used to report the power draw a smart plug
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PowerMessage {
|
||||
power: f32,
|
||||
}
|
||||
|
||||
impl PowerMessage {
|
||||
pub fn power(&self) -> f32 {
|
||||
self.power
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for PowerMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// Message used to report the power state of a hue light
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct HueState {
|
||||
on: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct HueMessage {
|
||||
state: HueState,
|
||||
}
|
||||
|
||||
impl HueMessage {
|
||||
pub fn is_on(&self) -> bool {
|
||||
self.state.on
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Bytes> for HueMessage {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&bytes).or(Err(ParseError::InvalidPayload(bytes.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Import this from the air_filter code itself instead of copying
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AirFilterFanState {
|
||||
Off,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct SetAirFilterFanState {
|
||||
state: AirFilterFanState,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct AirFilterState {
|
||||
pub state: AirFilterFanState,
|
||||
pub humidity: f32,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
impl SetAirFilterFanState {
|
||||
pub fn new(state: AirFilterFanState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for AirFilterState {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(message: Publish) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&message.payload)
|
||||
.or(Err(ParseError::InvalidPayload(message.payload.clone())))
|
||||
}
|
||||
}
|
||||
48
src/mqtt.rs
48
src/mqtt.rs
@@ -1,48 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use mlua::FromLua;
|
||||
use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::event::{self, EventChannel};
|
||||
|
||||
#[derive(Debug, Clone, FromLua)]
|
||||
pub struct WrappedAsyncClient(pub AsyncClient);
|
||||
|
||||
impl Deref for WrappedAsyncClient {
|
||||
type Target = AsyncClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for WrappedAsyncClient {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl mlua::UserData for WrappedAsyncClient {}
|
||||
|
||||
pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
|
||||
let tx = event_channel.get_tx();
|
||||
|
||||
tokio::spawn(async move {
|
||||
debug!("Listening for MQTT events");
|
||||
loop {
|
||||
let notification = eventloop.poll().await;
|
||||
match notification {
|
||||
Ok(Event::Incoming(Incoming::Publish(p))) => {
|
||||
tx.send(event::Event::MqttMessage(p)).await.ok();
|
||||
}
|
||||
Ok(..) => continue,
|
||||
Err(err) => {
|
||||
// Something has gone wrong
|
||||
// We stay in the loop as that will attempt to reconnect
|
||||
warn!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Action {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
pub type Schedule = IndexMap<String, IndexMap<Action, Vec<String>>>;
|
||||
|
||||
// #[derive(Debug, Deserialize)]
|
||||
// pub struct Schedule {
|
||||
// pub when: String,
|
||||
// pub actions: IndexMap<Action, Vec<String>>,
|
||||
// }
|
||||
@@ -1,10 +1,78 @@
|
||||
use std::result;
|
||||
|
||||
use axum::async_trait;
|
||||
use axum::extract::{FromRef, FromRequestParts};
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::status::InvalidStatusCode;
|
||||
use axum::http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use axum::response::IntoResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::error::{ApiError, ApiErrorJson};
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{source}")]
|
||||
pub struct ApiError {
|
||||
status_code: axum::http::StatusCode,
|
||||
source: Box<dyn std::error::Error>,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn new(status_code: axum::http::StatusCode, source: Box<dyn std::error::Error>) -> Self {
|
||||
Self {
|
||||
status_code,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for ApiErrorJson {
|
||||
fn from(value: ApiError) -> Self {
|
||||
let error = ApiErrorJsonError {
|
||||
code: value.status_code.as_u16(),
|
||||
status: value.status_code.to_string(),
|
||||
reason: value.source.to_string(),
|
||||
};
|
||||
|
||||
Self { error }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
self.status_code,
|
||||
serde_json::to_string::<ApiErrorJson>(&self.into())
|
||||
.expect("Serialization should not fail"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ApiErrorJsonError {
|
||||
code: u16,
|
||||
status: String,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiErrorJson {
|
||||
error: ApiErrorJsonError,
|
||||
}
|
||||
|
||||
impl TryFrom<ApiErrorJson> for ApiError {
|
||||
type Error = InvalidStatusCode;
|
||||
|
||||
fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> {
|
||||
let status_code = axum::http::StatusCode::from_u16(value.error.code)?;
|
||||
let source = value.error.reason.into();
|
||||
|
||||
Ok(Self {
|
||||
status_code,
|
||||
source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
Reference in New Issue
Block a user