cleanup rf formatting

This commit is contained in:
Robin Appelman 2026-06-27 00:13:40 +02:00
commit 16993948cf
4 changed files with 166 additions and 153 deletions

View file

@ -50,7 +50,7 @@ impl MiTempDevice {
}
pub fn format_prometheus(&self) -> impl Display + '_ {
MitempFormatter { device: &self }
MitempFormatter { device: self }
}
}

View file

@ -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<BDAddr, String>,
@ -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<W: Write>(
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<W: Write>(
mut writer: W,
channel: &RfDeviceId,
names: &HashMap<RfDeviceId, String>,
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<W: Write>(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<W: Write>(
)?;
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = <Cow<'de, 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<Self, Self::Err> {
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<RfPayload<'a>> {
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()
)
}

151
src/device/rf.rs Normal file
View file

@ -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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = <Cow<'de, 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<Self, Self::Err> {
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<RfPayload<'a>> {
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()
)
}