mirror of
https://codeberg.org/icewind/taspromto.git
synced 2026-08-02 12:14:45 +02:00
cleanup rf formatting
This commit is contained in:
parent
a9a2f5270e
commit
16993948cf
4 changed files with 166 additions and 153 deletions
|
|
@ -50,7 +50,7 @@ impl MiTempDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_prometheus(&self) -> impl Display + '_ {
|
pub fn format_prometheus(&self) -> impl Display + '_ {
|
||||||
MitempFormatter { device: &self }
|
MitempFormatter { device: self }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
mod mitemp;
|
mod mitemp;
|
||||||
|
mod rf;
|
||||||
|
|
||||||
pub use crate::device::mitemp::MiTempDevice;
|
|
||||||
use color_eyre::{Report, Result, eyre::WrapErr};
|
use color_eyre::{Report, Result, eyre::WrapErr};
|
||||||
use jzon::JsonValue;
|
use jzon::JsonValue;
|
||||||
|
pub use mitemp::MiTempDevice;
|
||||||
|
pub use rf::{RfDeviceId, TempState};
|
||||||
use rumqttc::{AsyncClient, QoS};
|
use rumqttc::{AsyncClient, QoS};
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
@ -10,11 +12,11 @@ use std::borrow::Cow;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||||
use std::num::ParseIntError;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tokio::task::spawn;
|
use tokio::task::spawn;
|
||||||
|
|
||||||
|
use crate::device::rf::parse_rf_payload;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DeviceStates {
|
pub struct DeviceStates {
|
||||||
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
||||||
|
|
@ -92,7 +94,7 @@ impl DeviceStates {
|
||||||
let state = self
|
let state = self
|
||||||
.rf_temp_devices
|
.rf_temp_devices
|
||||||
.entry(data.device_id())
|
.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.humidity = data.humidity;
|
||||||
state.temperature = data.temperature;
|
state.temperature = data.temperature;
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +123,7 @@ impl DeviceStates {
|
||||||
let state = self
|
let state = self
|
||||||
.rf_temp_devices
|
.rf_temp_devices
|
||||||
.entry(self.active_rf_temp_id.clone())
|
.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 {
|
match field {
|
||||||
"temperature_C" => state.temperature = payload.parse().unwrap_or_default(),
|
"temperature_C" => state.temperature = payload.parse().unwrap_or_default(),
|
||||||
"temperature_F" => {
|
"temperature_F" => {
|
||||||
|
|
@ -420,53 +422,6 @@ pub fn format_device_state<W: Write>(
|
||||||
Ok(())
|
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 {
|
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()
|
let power_total = state.power_total_tariff_1.unwrap_or_default()
|
||||||
+ state.power_total_tariff_2.unwrap_or_default();
|
+ state.power_total_tariff_2.unwrap_or_default();
|
||||||
|
|
@ -733,91 +688,3 @@ pub fn format_pms_state<W: Write>(
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
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
151
src/device/rf.rs
Normal 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
19
src/main.rs
19
src/main.rs
|
|
@ -4,9 +4,7 @@ mod mqtt;
|
||||||
mod topic;
|
mod topic;
|
||||||
|
|
||||||
use crate::config::{Config, ListenConfig};
|
use crate::config::{Config, ListenConfig};
|
||||||
use crate::device::{
|
use crate::device::{Device, DeviceStates, format_device_state, format_dsmr_state};
|
||||||
Device, DeviceStates, format_device_state, format_dsmr_state, format_rf_temp_state,
|
|
||||||
};
|
|
||||||
use crate::mqtt::mqtt_stream;
|
use crate::mqtt::mqtt_stream;
|
||||||
use crate::topic::Topic;
|
use crate::topic::Topic;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
@ -43,8 +41,7 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
let mqtt_options = config.mqtt()?;
|
let mqtt_options = config.mqtt()?;
|
||||||
|
|
||||||
let device_states =
|
let device_states = DeviceStates::new(config.names.mi_temp, config.names.rf_temp);
|
||||||
DeviceStates::new(config.names.mi_temp.clone(), config.names.rf_temp.clone());
|
|
||||||
let device_states = Arc::new(Mutex::new(device_states));
|
let device_states = Arc::new(Mutex::new(device_states));
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
|
|
@ -52,7 +49,7 @@ async fn main() -> Result<()> {
|
||||||
})
|
})
|
||||||
.expect("Error setting Ctrl-C handler");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
spawn(serve(device_states.clone(), config));
|
spawn(serve(device_states.clone(), config.listen));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (client, stream) = mqtt_stream(mqtt_options.clone())
|
let (client, stream) = mqtt_stream(mqtt_options.clone())
|
||||||
|
|
@ -73,9 +70,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
|
async fn serve(device_states: Arc<Mutex<DeviceStates>>, listen: ListenConfig) {
|
||||||
let rf_temp_names = config.names.rf_temp;
|
|
||||||
|
|
||||||
let state = warp::any().map(move || device_states.clone());
|
let state = warp::any().map(move || device_states.clone());
|
||||||
|
|
||||||
let metrics = warp::path!("metrics")
|
let metrics = warp::path!("metrics")
|
||||||
|
|
@ -92,14 +87,14 @@ async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
|
||||||
for (_, state) in state.mi_temp() {
|
for (_, state) in state.mi_temp() {
|
||||||
write!(&mut response, "{}", state.format_prometheus()).expect("formatting failed");
|
write!(&mut response, "{}", state.format_prometheus()).expect("formatting failed");
|
||||||
}
|
}
|
||||||
for (channel, state) in state.rf_temp() {
|
for (_, state) in state.rf_temp() {
|
||||||
format_rf_temp_state(&mut response, channel, &rf_temp_names, state).unwrap()
|
write!(&mut response, "{}", state.format_prometheus()).expect("formatting failed");
|
||||||
}
|
}
|
||||||
response
|
response
|
||||||
});
|
});
|
||||||
|
|
||||||
let cancel = async { ctrl_c().await.unwrap() };
|
let cancel = async { ctrl_c().await.unwrap() };
|
||||||
match config.listen {
|
match listen {
|
||||||
ListenConfig::Ip { address, port } => {
|
ListenConfig::Ip { address, port } => {
|
||||||
warp::serve(metrics)
|
warp::serve(metrics)
|
||||||
.bind((address, port))
|
.bind((address, port))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue