diff --git a/src/device/mitemp.rs b/src/device/mitemp.rs index 786f7bc..6ee237d 100644 --- a/src/device/mitemp.rs +++ b/src/device/mitemp.rs @@ -50,7 +50,7 @@ impl MiTempDevice { } pub fn format_prometheus(&self) -> impl Display + '_ { - MitempFormatter { device: &self } + MitempFormatter { device: self } } } diff --git a/src/device/mod.rs b/src/device/mod.rs index e7b85f3..2ee89f5 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,8 +1,10 @@ mod mitemp; +mod rf; -pub use crate::device::mitemp::MiTempDevice; use color_eyre::{Report, Result, eyre::WrapErr}; use jzon::JsonValue; +pub use mitemp::MiTempDevice; +pub use rf::{RfDeviceId, TempState}; use rumqttc::{AsyncClient, QoS}; use serde::de::Error; use serde::{Deserialize, Deserializer}; @@ -10,11 +12,11 @@ use std::borrow::Cow; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::fmt::{self, Debug, Display, Formatter, Write}; -use std::num::ParseIntError; -use std::str::FromStr; use std::time::Instant; use tokio::task::spawn; +use crate::device::rf::parse_rf_payload; + #[derive(Default)] pub struct DeviceStates { pub mi_temp_names: BTreeMap, @@ -92,7 +94,7 @@ impl DeviceStates { let state = self .rf_temp_devices .entry(data.device_id()) - .or_insert_with(|| TempState::new(name.clone())); + .or_insert_with(|| TempState::new(id.clone(), name.clone())); state.humidity = data.humidity; state.temperature = data.temperature; } @@ -121,7 +123,7 @@ impl DeviceStates { let state = self .rf_temp_devices .entry(self.active_rf_temp_id.clone()) - .or_insert_with(|| TempState::new(name.clone())); + .or_insert_with(|| TempState::new(self.active_rf_temp_id.clone(), name.clone())); match field { "temperature_C" => state.temperature = payload.parse().unwrap_or_default(), "temperature_F" => { @@ -420,53 +422,6 @@ pub fn format_device_state( Ok(()) } -#[derive(Debug)] -pub struct TempState { - name: String, - temperature: f32, - humidity: u8, -} - -impl TempState { - pub fn new(name: String) -> Self { - TempState { - name, - temperature: 0.0, - humidity: 0, - } - } -} - -pub fn format_rf_temp_state( - mut writer: W, - channel: &RfDeviceId, - names: &HashMap, - state: &TempState, -) -> fmt::Result { - let name = if let Some(name) = names.get(channel) { - name - } else { - return Ok(()); - }; - - if state.temperature > 0.0 { - writeln!( - writer, - "sensor_temperature{{model=\"{}\", id=\"{}\", channel=\"{}\", name=\"{}\"}} {}", - channel.name, channel.id, channel.channel, name, state.temperature - )?; - } - - if state.humidity > 0 { - writeln!( - writer, - "sensor_humidity{{model=\"{}\", id=\"{}\", channel=\"{}\", name=\"{}\"}} {}", - channel.name, channel.id, channel.channel, name, state.humidity - )?; - } - Ok(()) -} - pub fn format_dsmr_state(mut writer: W, device: &str, state: &DsmrState) -> fmt::Result { let power_total = state.power_total_tariff_1.unwrap_or_default() + state.power_total_tariff_2.unwrap_or_default(); @@ -733,91 +688,3 @@ pub fn format_pms_state( )?; Ok(()) } - -#[derive(Debug, PartialEq)] -struct RfPayload<'a> { - name: &'a str, - id: u16, - channel: u8, - battery: bool, - temperature: f32, - humidity: u8, -} - -impl<'a> RfPayload<'a> { - pub fn device_id(&self) -> RfDeviceId { - RfDeviceId { - name: self.name.into(), - id: self.id, - channel: self.channel, - } - } -} - -#[derive(Hash, PartialEq, Eq, Debug, Default, Clone)] -pub struct RfDeviceId { - name: String, - id: u16, - channel: u8, -} - -impl<'de> Deserialize<'de> for RfDeviceId { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - let str = >::deserialize(deserializer)?; - Self::from_str(&str).map_err(D::Error::custom) - } -} - -impl FromStr for RfDeviceId { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - let mut parts = s.splitn(3, ':'); - let name = parts.next().unwrap_or_default(); - let id = parts.next().unwrap_or_default().parse()?; - let channel = parts.next().unwrap_or_default().parse()?; - Ok(RfDeviceId { - name: name.into(), - id, - channel, - }) - } -} - -fn parse_rf_payload<'a>(payload: &'a str) -> Option> { - let mut parts = payload.split(";").skip(2); - let name = parts.next()?; - let id = parts.next()?.strip_prefix("ID=")?.parse().ok()?; - let channel = parts.next()?.strip_prefix("CHN=")?.parse().ok()?; - let battery = parts.next()?.strip_prefix("BAT=")? == "OK"; - let temperature = parts.next()?.strip_prefix("TEMP=")?; - let temperature = u32::from_str_radix(temperature, 16).ok()?; - let humidity = parts.next()?.strip_prefix("HUM=")?.parse().ok()?; - - Some(RfPayload { - name, - id, - channel, - battery, - temperature: temperature as f32 / 10.0, - humidity, - }) -} - -#[test] -fn test_rf_payload() { - assert_eq!( - RfPayload { - name: "Bresser-3CH", - id: 49, - channel: 1, - battery: true, - temperature: 16.1, - humidity: 58 - }, - parse_rf_payload("20;1E;Bresser-3CH;ID=49;CHN=0001;BAT=OK;TEMP=00a1;HUM=58;").unwrap() - ) -} diff --git a/src/device/rf.rs b/src/device/rf.rs new file mode 100644 index 0000000..97586ed --- /dev/null +++ b/src/device/rf.rs @@ -0,0 +1,151 @@ +use std::borrow::Cow; +use std::fmt::{self, Display}; +use std::num::ParseIntError; +use std::str::FromStr; + +use serde::{Deserialize, Deserializer, de::Error}; + +#[derive(Debug)] +pub struct TempState { + pub name: String, + pub id: RfDeviceId, + pub temperature: f32, + pub humidity: u8, +} + +impl TempState { + pub fn new(id: RfDeviceId, name: String) -> Self { + TempState { + name, + id, + temperature: 0.0, + humidity: 0, + } + } + + pub fn format_prometheus(&self) -> impl Display + '_ { + TempFormatter { device: self } + } +} + +struct TempFormatter<'a> { + device: &'a TempState, +} + +impl Display for TempFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.device.temperature > 0.0 { + writeln!( + f, + "sensor_temperature{{model=\"{}\", id=\"{}\", channel=\"{}\", name=\"{}\"}} {}", + self.device.id.name, + self.device.id.id, + self.device.id.channel, + self.device.name, + self.device.temperature + )?; + } + + if self.device.humidity > 0 { + writeln!( + f, + "sensor_humidity{{model=\"{}\", id=\"{}\", channel=\"{}\", name=\"{}\"}} {}", + self.device.id.name, + self.device.id.id, + self.device.id.channel, + self.device.name, + self.device.humidity + )?; + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq)] +pub struct RfPayload<'a> { + pub name: &'a str, + pub id: u16, + pub channel: u8, + pub battery: bool, + pub temperature: f32, + pub humidity: u8, +} + +impl<'a> RfPayload<'a> { + pub fn device_id(&self) -> RfDeviceId { + RfDeviceId { + name: self.name.into(), + id: self.id, + channel: self.channel, + } + } +} + +#[derive(Hash, PartialEq, Eq, Debug, Default, Clone)] +pub struct RfDeviceId { + pub name: String, + pub id: u16, + pub channel: u8, +} + +impl<'de> Deserialize<'de> for RfDeviceId { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let str = >::deserialize(deserializer)?; + Self::from_str(&str).map_err(D::Error::custom) + } +} + +impl FromStr for RfDeviceId { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let mut parts = s.splitn(3, ':'); + let name = parts.next().unwrap_or_default(); + let id = parts.next().unwrap_or_default().parse()?; + let channel = parts.next().unwrap_or_default().parse()?; + Ok(RfDeviceId { + name: name.into(), + id, + channel, + }) + } +} + +pub fn parse_rf_payload<'a>(payload: &'a str) -> Option> { + let mut parts = payload.split(";").skip(2); + let name = parts.next()?; + let id = parts.next()?.strip_prefix("ID=")?.parse().ok()?; + let channel = parts.next()?.strip_prefix("CHN=")?.parse().ok()?; + let battery = parts.next()?.strip_prefix("BAT=")? == "OK"; + let temperature = parts.next()?.strip_prefix("TEMP=")?; + let temperature = u32::from_str_radix(temperature, 16).ok()?; + let humidity = parts.next()?.strip_prefix("HUM=")?.parse().ok()?; + + Some(RfPayload { + name, + id, + channel, + battery, + temperature: temperature as f32 / 10.0, + humidity, + }) +} + +#[test] +fn test_rf_payload() { + assert_eq!( + RfPayload { + name: "Bresser-3CH", + id: 49, + channel: 1, + battery: true, + temperature: 16.1, + humidity: 58 + }, + parse_rf_payload("20;1E;Bresser-3CH;ID=49;CHN=0001;BAT=OK;TEMP=00a1;HUM=58;").unwrap() + ) +} diff --git a/src/main.rs b/src/main.rs index ad5fd15..011e777 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,7 @@ mod mqtt; mod topic; use crate::config::{Config, ListenConfig}; -use crate::device::{ - Device, DeviceStates, format_device_state, format_dsmr_state, format_rf_temp_state, -}; +use crate::device::{Device, DeviceStates, format_device_state, format_dsmr_state}; use crate::mqtt::mqtt_stream; use crate::topic::Topic; use clap::Parser; @@ -43,8 +41,7 @@ async fn main() -> Result<()> { }; let mqtt_options = config.mqtt()?; - let device_states = - DeviceStates::new(config.names.mi_temp.clone(), config.names.rf_temp.clone()); + let device_states = DeviceStates::new(config.names.mi_temp, config.names.rf_temp); let device_states = Arc::new(Mutex::new(device_states)); ctrlc::set_handler(move || { @@ -52,7 +49,7 @@ async fn main() -> Result<()> { }) .expect("Error setting Ctrl-C handler"); - spawn(serve(device_states.clone(), config)); + spawn(serve(device_states.clone(), config.listen)); loop { let (client, stream) = mqtt_stream(mqtt_options.clone()) @@ -73,9 +70,7 @@ async fn main() -> Result<()> { } } -async fn serve(device_states: Arc>, config: Config) { - let rf_temp_names = config.names.rf_temp; - +async fn serve(device_states: Arc>, listen: ListenConfig) { let state = warp::any().map(move || device_states.clone()); let metrics = warp::path!("metrics") @@ -92,14 +87,14 @@ async fn serve(device_states: Arc>, config: Config) { for (_, state) in state.mi_temp() { write!(&mut response, "{}", state.format_prometheus()).expect("formatting failed"); } - for (channel, state) in state.rf_temp() { - format_rf_temp_state(&mut response, channel, &rf_temp_names, state).unwrap() + for (_, state) in state.rf_temp() { + write!(&mut response, "{}", state.format_prometheus()).expect("formatting failed"); } response }); let cancel = async { ctrl_c().await.unwrap() }; - match config.listen { + match listen { ListenConfig::Ip { address, port } => { warp::serve(metrics) .bind((address, port))