Reworked air filter integration
All checks were successful
Build and deploy / Build application (push) Successful in 5m8s
Build and deploy / Build container (push) Successful in 2m19s
Build and deploy / Deploy container (push) Successful in 35s

This commit is contained in:
Dreaded_X 2025-01-22 03:12:13 +01:00
parent 5af713cf8f
commit 3905df690b
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
13 changed files with 250 additions and 160 deletions

180
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -38,6 +38,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "air_filter_types"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/airfilter?tag=v0.4.4#8603d6fab4ceee29c68db1edd556d691f123842d"
dependencies = [
"bme280",
"defmt",
"serde",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
@ -79,7 +89,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -118,6 +128,7 @@ version = "0.1.0"
name = "automation_devices"
version = "0.1.0"
dependencies = [
"air_filter_types",
"anyhow",
"async-trait",
"automation_cast",
@ -176,7 +187,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -267,6 +278,18 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bme280"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "169ac81b9123f316fde5b0e00175294dcdcdd800c1a6c92a4b58caf42a14cf1f"
dependencies = [
"defmt",
"embedded-hal",
"embedded-hal-async",
"maybe-async-cfg",
]
[[package]]
name = "bstr"
version = "1.11.0"
@ -355,6 +378,38 @@ dependencies = [
"chrono",
]
[[package]]
name = "defmt"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6"
dependencies = [
"defmt-parser",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "defmt-parser"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3"
dependencies = [
"thiserror 2.0.5",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@ -373,6 +428,21 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "embedded-hal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
[[package]]
name = "embedded-hal-async"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
dependencies = [
"embedded-hal",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -491,7 +561,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -564,7 +634,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -848,12 +918,49 @@ dependencies = [
"which",
]
[[package]]
name = "manyhow"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
dependencies = [
"manyhow-macros",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.90",
]
[[package]]
name = "manyhow-macros"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
dependencies = [
"proc-macro-utils",
"proc-macro2",
"quote",
]
[[package]]
name = "matchit"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
[[package]]
name = "maybe-async-cfg"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dbfaa67a76e2623580df07d6bb5e7956c0a4bae4b418314083a9c619bd66627"
dependencies = [
"manyhow",
"proc-macro2",
"pulldown-cmark",
"quote",
"syn 1.0.109",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -930,7 +1037,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
"syn 2.0.90",
]
[[package]]
@ -951,7 +1058,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1080,7 +1187,18 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
name = "proc-macro-utils"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
dependencies = [
"proc-macro2",
"quote",
"smallvec",
]
[[package]]
@ -1092,6 +1210,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625"
dependencies = [
"bitflags 2.5.0",
"memchr",
"unicase",
]
[[package]]
name = "quinn"
version = "0.11.6"
@ -1468,7 +1597,7 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1500,7 +1629,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1570,6 +1699,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.90"
@ -1622,7 +1762,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1633,7 +1773,7 @@ checksum = "995d0bbc9995d1f19d28b7215a9352b0fc3cd3a2d2ec95c2cadc485cdedbcdde"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1700,7 +1840,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1790,7 +1930,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1840,6 +1980,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
@ -1945,7 +2091,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
"wasm-bindgen-shared",
]
@ -1979,7 +2125,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2274,7 +2420,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]

View File

@ -65,6 +65,7 @@ tracing-subscriber = "0.3.16"
uuid = "1.8.0"
wakey = "0.3.0"
zigbee2mqtt-types = { version = "0.4.0", features = ["debug", "philips"] }
air_filter_types = { git = "https://git.huizinga.dev/Dreaded_X/airfilter", tag = "v0.4.4" }
[dependencies]
automation_lib = { workspace = true }

View File

@ -25,3 +25,4 @@ bytes = { workspace = true }
thiserror = { workspace = true }
eui48 = { workspace = true }
wakey = { workspace = true }
air_filter_types = { workspace = true }

View File

@ -1,11 +1,6 @@
use std::sync::Arc;
use async_trait::async_trait;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::config::InfoConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig;
use google_home::device::Name;
use google_home::errors::ErrorCode;
@ -14,51 +9,57 @@ use google_home::traits::{
TemperatureUnit,
};
use google_home::types::Type;
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace, warn};
use thiserror::Error;
use tracing::{debug, trace};
#[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,
pub url: String,
}
#[derive(Debug, Clone)]
pub struct AirFilter {
config: Config,
state: Arc<RwLock<AirFilterState>>,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Connection error")]
ReqwestError(#[from] reqwest::Error),
}
impl From<Error> for google_home::errors::ErrorCode {
fn from(value: Error) -> Self {
match value {
// Assume that if we encounter a ReqwestError the device is offline
Error::ReqwestError(_) => {
Self::DeviceError(google_home::errors::DeviceError::DeviceOffline)
}
}
}
}
// TODO: Handle error properly
impl AirFilter {
async fn set_speed(&self, state: AirFilterFanState) {
let message = SetAirFilterFanState::new(state);
async fn set_fan_speed(&self, speed: air_filter_types::FanSpeed) -> Result<(), Error> {
let message = air_filter_types::SetFanSpeed::new(speed);
let url = format!("{}/state/fan", self.config.url);
let client = reqwest::Client::new();
client.put(url).json(&message).send().await?;
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(())
}
async fn state(&self) -> RwLockReadGuard<AirFilterState> {
self.state.read().await
async fn get_fan_state(&self) -> Result<air_filter_types::FanState, Error> {
let url = format!("{}/state/fan", self.config.url);
Ok(reqwest::get(url).await?.json().await?)
}
async fn state_mut(&self) -> RwLockWriteGuard<AirFilterState> {
self.state.write().await
async fn get_sensor_data(&self) -> Result<air_filter_types::SensorData, Error> {
let url = format!("{}/state/sensor", self.config.url);
Ok(reqwest::get(url).await?.json().await?)
}
}
@ -70,19 +71,7 @@ impl LuaDeviceCreate for AirFilter {
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 })
Ok(Self { config })
}
}
@ -93,30 +82,6 @@ impl Device for AirFilter {
}
#[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
@ -130,8 +95,8 @@ impl google_home::Device for AirFilter {
Device::get_id(self)
}
fn is_online(&self) -> bool {
true
async fn is_online(&self) -> bool {
self.get_sensor_data().await.is_ok()
}
fn get_room_hint(&self) -> Option<&str> {
@ -146,16 +111,16 @@ impl google_home::Device for AirFilter {
#[async_trait]
impl OnOff for AirFilter {
async fn on(&self) -> Result<bool, ErrorCode> {
Ok(self.state().await.state != AirFilterFanState::Off)
Ok(self.get_fan_state().await?.speed != air_filter_types::FanSpeed::Off)
}
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
debug!("Turning on air filter: {on}");
if on {
self.set_speed(AirFilterFanState::High).await;
self.set_fan_speed(air_filter_types::FanSpeed::High).await?;
} else {
self.set_speed(AirFilterFanState::Off).await;
self.set_fan_speed(air_filter_types::FanSpeed::Off).await?;
}
Ok(())
@ -201,11 +166,12 @@ impl FanSpeed for AirFilter {
}
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",
let speed = self.get_fan_state().await?.speed;
let speed = match speed {
air_filter_types::FanSpeed::Off => "off",
air_filter_types::FanSpeed::Low => "low",
air_filter_types::FanSpeed::Medium => "medium",
air_filter_types::FanSpeed::High => "high",
};
Ok(speed.into())
@ -213,19 +179,19 @@ impl FanSpeed for AirFilter {
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
let speed = if fan_speed == "off" {
air_filter_types::FanSpeed::Off
} else if fan_speed == "low" {
AirFilterFanState::Low
air_filter_types::FanSpeed::Low
} else if fan_speed == "medium" {
AirFilterFanState::Medium
air_filter_types::FanSpeed::Medium
} else if fan_speed == "high" {
AirFilterFanState::High
air_filter_types::FanSpeed::High
} else {
return Err(google_home::errors::DeviceError::TransientError.into());
};
self.set_speed(state).await;
self.set_fan_speed(speed).await?;
Ok(())
}
@ -238,7 +204,7 @@ impl HumiditySetting for AirFilter {
}
async fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode> {
Ok(self.state().await.humidity.round() as isize)
Ok(self.get_sensor_data().await?.humidity().round() as isize)
}
}
@ -253,8 +219,8 @@ impl TemperatureSetting for AirFilter {
TemperatureUnit::Celsius
}
async fn temperature_ambient_celsius(&self) -> f32 {
async fn temperature_ambient_celsius(&self) -> Result<f32, ErrorCode> {
// HACK: Round to one decimal place
(10.0 * self.state().await.temperature).round() / 10.0
Ok((10.0 * self.get_sensor_data().await?.temperature()).round() / 10.0)
}
}

View File

@ -107,6 +107,7 @@ impl Device for ContactSensor {
}
}
#[async_trait]
impl google_home::Device for ContactSensor {
fn get_device_type(&self) -> google_home::types::Type {
match self.config.sensor_type {
@ -132,7 +133,7 @@ impl google_home::Device for ContactSensor {
false
}
fn is_online(&self) -> bool {
async fn is_online(&self) -> bool {
true
}
}

View File

@ -127,6 +127,7 @@ impl OnPresence for IkeaOutlet {
}
}
#[async_trait]
impl google_home::Device for IkeaOutlet {
fn get_device_type(&self) -> Type {
match self.config.outlet_type {
@ -144,7 +145,7 @@ impl google_home::Device for IkeaOutlet {
Device::get_id(self)
}
fn is_online(&self) -> bool {
async fn is_online(&self) -> bool {
true
}

View File

@ -75,6 +75,7 @@ impl OnMqtt for WakeOnLAN {
}
}
#[async_trait]
impl google_home::Device for WakeOnLAN {
fn get_device_type(&self) -> Type {
Type::Scene
@ -91,7 +92,7 @@ impl google_home::Device for WakeOnLAN {
Device::get_id(self)
}
fn is_online(&self) -> bool {
async fn is_online(&self) -> bool {
true
}

View File

@ -191,6 +191,7 @@ impl<T: LightState> OnPresence for Light<T> {
}
}
#[async_trait]
impl<T: LightState> google_home::Device for Light<T> {
fn get_device_type(&self) -> Type {
Type::Light
@ -204,7 +205,7 @@ impl<T: LightState> google_home::Device for Light<T> {
Device::get_id(self)
}
fn is_online(&self) -> bool {
async fn is_online(&self) -> bool {
true
}

View File

@ -241,40 +241,3 @@ impl TryFrom<Bytes> for HueMessage {
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())))
}
}

View File

@ -364,8 +364,7 @@ automation.device_manager:add(LightOnOff.new({
local bedroom_air_filter = AirFilter.new({
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = mqtt_client,
url = "http://airfilter.lan.huizinga.dev",
})
automation.device_manager:add(bedroom_air_filter)

View File

@ -11,7 +11,7 @@ pub trait Device: DeviceFulfillment {
fn get_device_type(&self) -> Type;
fn get_device_name(&self) -> Name;
fn get_id(&self) -> String;
fn is_online(&self) -> bool;
async fn is_online(&self) -> bool;
// Default values that can optionally be overridden
fn will_report_state(&self) -> bool {
@ -37,29 +37,39 @@ pub trait Device: DeviceFulfillment {
}
device.device_info = self.get_device_info();
let (traits, attributes) = DeviceFulfillment::sync(self).await.unwrap();
// TODO: Return the appropriate error
if let Ok((traits, attributes)) = DeviceFulfillment::sync(self).await {
device.traits = traits;
device.attributes = attributes;
}
device
}
async fn query(&self) -> response::query::Device {
let mut device = response::query::Device::new();
if !self.is_online() {
if !self.is_online().await {
device.set_offline();
}
device.state = DeviceFulfillment::query(self).await.unwrap();
// TODO: Return the appropriate error
if let Ok(state) = DeviceFulfillment::query(self).await {
device.state = state;
}
device
}
async fn execute(&self, command: Command) -> Result<(), ErrorCode> {
DeviceFulfillment::execute(self, command.clone())
// TODO: Do something with the return value, or just get rut of the return value?
if DeviceFulfillment::execute(self, command.clone())
.await
.unwrap();
.is_err()
{
return Err(ErrorCode::DeviceError(
crate::errors::DeviceError::TransientError,
));
}
Ok(())
}

View File

@ -140,7 +140,7 @@ impl GoogleHome {
if let Some(device) = devices.get(id.as_str())
&& let Some(device) = device.as_ref().cast()
{
if !device.is_online() {
if !device.is_online().await {
return (id, Ok(false));
}

View File

@ -52,7 +52,7 @@ traits! {
// TODO: Add rename
temperatureUnitForUX: TemperatureUnit,
async fn temperature_ambient_celsius(&self) -> f32,
async fn temperature_ambient_celsius(&self) -> Result<f32, ErrorCode>,
}
}