diff --git a/src/device/mod.rs b/src/device/mod.rs index 0a94600..614f66b 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,11 +1,13 @@ mod dsmr; mod mitemp; mod rf; +mod tasmota; use color_eyre::{Report, Result, eyre::WrapErr}; pub use dsmr::{DsmrMessageType, DsmrState}; use jzon::JsonValue; pub use mitemp::MiTempDevice; +use rf::parse_rf_payload; pub use rf::{RfDeviceId, TempState}; use rumqttc::{AsyncClient, QoS}; use serde::de::Error; @@ -13,12 +15,11 @@ use serde::{Deserialize, Deserializer}; use std::borrow::Cow; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; -use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::fmt::{self, Debug, Display, Formatter}; use std::time::Instant; +pub use tasmota::DeviceState; use tokio::task::spawn; -use crate::device::rf::parse_rf_payload; - #[derive(Default)] pub struct DeviceStates { pub mi_temp_names: BTreeMap, @@ -52,7 +53,10 @@ impl DeviceStates { } pub fn update(&mut self, device: Device, json: JsonValue) { - let device = self.devices.entry(device).or_default(); + let device = self + .devices + .entry(device) + .or_insert_with_key(|device| DeviceState::new(device.hostname.clone())); for (key, value) in json.entries() { if let Some(addr) = key.strip_prefix("MJ_HT_V1") { @@ -196,206 +200,6 @@ impl Device { } } -#[derive(Debug)] -pub struct DeviceState { - pub state: Option, - pub name: String, - pub power_watts: Option, - pub power_yesterday: Option, - pub power_today: Option, - pub power_total: Option, - pub power_total_low: Option, - pub power_total_high: Option, - pub gas_total: Option, - pub co2: Option, - pub pms_state: Option, - pub last_seen: Instant, - pub firmware: String, - pub version: f32, -} - -impl Default for DeviceState { - fn default() -> Self { - DeviceState { - state: Default::default(), - name: Default::default(), - power_watts: Default::default(), - power_yesterday: Default::default(), - power_today: Default::default(), - power_total: Default::default(), - power_total_low: Default::default(), - power_total_high: Default::default(), - gas_total: Default::default(), - co2: Default::default(), - pms_state: Default::default(), - last_seen: Instant::now(), - firmware: Default::default(), - version: 0.0, - } - } -} - -impl DeviceState { - pub fn update(&mut self, json: JsonValue) { - self.last_seen = Instant::now(); - - if json["DeviceName"].is_string() && !json["DeviceName"].is_empty() { - self.name = json["DeviceName"].to_string(); - } - if json["POWER"].is_string() && !json["POWER"].is_empty() { - self.state = Some(json["POWER"] == "ON"); - } - if let Some(power) = json["ENERGY"]["Power"].as_number().map(f32::from) { - self.power_watts = Some(power); - } - if let Some(yesterday) = json["ENERGY"]["Yesterday"].as_number().map(f32::from) { - self.power_yesterday = Some(yesterday); - } - if let Some(today) = json["ENERGY"]["Today"].as_number().map(f32::from) { - self.power_today = Some(today); - } - if let Some(co2) = json["MHZ19B"]["CarbonDioxide"].as_number().map(f32::from) { - if co2 > 1.0 { - self.co2 = Some(co2); - } - } - if let Some(power) = json["OBIS"]["Power"].as_number().map(f32::from) { - self.power_watts = Some(power); - } - if let Some(total) = json["OBIS"]["Total"].as_number().map(f32::from) { - self.power_total = Some(total); - } - if let Some(total) = json["OBIS"]["Total_high"].as_number().map(f32::from) { - self.power_total_high = Some(total); - } - if let Some(total) = json["OBIS"]["Total_low"].as_number().map(f32::from) { - self.power_total_low = Some(total); - } - if let Some(gas) = json["OBIS"]["Gas_total"].as_number().map(f32::from) { - self.gas_total = Some(gas); - } - - if let Some(version) = json["StatusFWR"]["Version"].as_str() { - self.firmware = version.into(); - if let Some(version) = version - .rfind('.') - .map(|index| &version[0..index]) - .and_then(|s| s.parse().ok()) - { - self.version = version - } - } - - if json["PMS5003"].is_object() { - let pms = self.pms_state.get_or_insert(PMSState::default()); - pms.update(&json["PMS5003"]); - } - } -} - -pub fn format_device_state( - mut writer: W, - device: &Device, - state: &DeviceState, -) -> fmt::Result { - if state.name.is_empty() { - println!("{} has no name set, skipping", device.hostname); - return Ok(()); - } - writeln!( - writer, - "tasmota_online{{tasmota_id=\"{}\", name=\"{}\"}} 1", - device.hostname, state.name - )?; - if let Some(switch_state) = state.state { - writeln!( - writer, - "switch_state{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, - state.name, - if switch_state { 1 } else { 0 } - )?; - } - - if let Some(power_watts) = state.power_watts { - writeln!( - writer, - "power_watts{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_watts - )?; - } - - if let Some(power_yesterday) = state.power_yesterday { - writeln!( - writer, - "power_yesterday_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_yesterday - )?; - } - - if let Some(power_today) = state.power_today { - writeln!( - writer, - "power_today_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_today - )?; - } - - if let Some(power_total) = state.power_total { - writeln!( - writer, - "power_total_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_total - )?; - } - - if let Some(power_total) = state.power_total_high { - writeln!( - writer, - "power_total_high_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_total - )?; - } - - if let Some(power_total) = state.power_total_low { - writeln!( - writer, - "power_total_low_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, power_total - )?; - } - - if let Some(gas_total) = state.gas_total { - writeln!( - writer, - "gas_total_m3{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, gas_total - )?; - } - - if let Some(co2) = state.co2 { - writeln!( - writer, - "sensor_co2{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, state.name, co2 - )?; - } - - if let Some(pms) = state.pms_state.as_ref() { - format_pms_state(&mut writer, device, state, pms)?; - } - - if !state.firmware.is_empty() { - writeln!( - writer, - r#"tasmota_version{{tasmota_id="{}", name="{}", firmware="{}", version="{}"}} 1"#, - device.hostname, state.name, state.firmware, state.version - )?; - } - - Ok(()) -} - /// Stores the 6 byte address used to identify Bluetooth devices. #[derive(Copy, Clone, Hash, Eq, PartialEq, Default, Ord, PartialOrd)] #[repr(C)] @@ -450,169 +254,3 @@ impl Debug for BDAddr { (self as &dyn Display).fmt(f) } } - -//"PMS5003":{"CF1":6,"CF2.5":8,"CF10":8,"PM1":6,"PM2.5":8,"PM10":8,"PB0.3":0,"PB0.5":0,"PB1":0,"PB2.5":0,"PB5":0,"PB10":0} - -#[derive(Debug, Clone, Default)] -pub struct PMSState { - cf1: u16, - cf2_5: u16, - cf10: u16, - pm1: u16, - pm2_5: u16, - pm10: u16, - pb0_3: u16, - pb0_5: u16, - pb1: u16, - pb2_5: u16, - pb5: u16, - pb10: u16, -} - -impl PMSState { - pub fn update(&mut self, json: &JsonValue) { - if let Some(val) = json["CF1"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.cf1 = val; - } - if let Some(val) = json["CF2.5"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.cf2_5 = val; - } - if let Some(val) = json["CF10"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.cf10 = val; - } - if let Some(val) = json["PM1"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pm1 = val; - } - if let Some(val) = json["PM2.5"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pm2_5 = val; - } - if let Some(val) = json["PM10"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pm10 = val; - } - if let Some(val) = json["PB0.3"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb0_3 = val; - } - if let Some(val) = json["PB0.5"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb0_5 = val; - } - if let Some(val) = json["PB1"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb1 = val; - } - if let Some(val) = json["PB2.5"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb2_5 = val; - } - if let Some(val) = json["PB5"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb5 = val; - } - if let Some(val) = json["PB10"] - .as_number() - .and_then(|num| u16::try_from(num).ok()) - { - self.pb10 = val; - } - } -} - -pub fn format_pms_state( - mut writer: W, - device: &Device, - device_state: &DeviceState, - state: &PMSState, -) -> fmt::Result { - let name = &device_state.name; - - writeln!( - writer, - "cf1{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.cf1 - )?; - writeln!( - writer, - "cf2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.cf2_5 - )?; - writeln!( - writer, - "cf10{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.cf10 - )?; - writeln!( - writer, - "pm1{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pm1 - )?; - writeln!( - writer, - "pm2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pm2_5 - )?; - writeln!( - writer, - "pm10{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pm10 - )?; - writeln!( - writer, - "pb0_3{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb0_3 - )?; - writeln!( - writer, - "pb0_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb0_5 - )?; - writeln!( - writer, - "pb1{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb1 - )?; - writeln!( - writer, - "pb2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb2_5 - )?; - writeln!( - writer, - "pb5{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb5 - )?; - writeln!( - writer, - "pb10{{tasmota_id=\"{}\", name=\"{}\"}} {}", - device.hostname, name, state.pb10 - )?; - Ok(()) -} diff --git a/src/device/tasmota.rs b/src/device/tasmota.rs new file mode 100644 index 0000000..3fd880e --- /dev/null +++ b/src/device/tasmota.rs @@ -0,0 +1,386 @@ +use std::{ + fmt::{self, Display}, + time::Instant, +}; + +use jzon::JsonValue; + +#[derive(Debug)] +pub struct DeviceState { + pub state: Option, + pub name: String, + pub hostname: String, + pub power_watts: Option, + pub power_yesterday: Option, + pub power_today: Option, + pub power_total: Option, + pub power_total_low: Option, + pub power_total_high: Option, + pub gas_total: Option, + pub co2: Option, + pub pms_state: Option, + pub last_seen: Instant, + pub firmware: String, + pub version: f32, +} + +impl DeviceState { + pub fn new(hostname: String) -> Self { + DeviceState { + hostname, + state: Default::default(), + name: Default::default(), + power_watts: Default::default(), + power_yesterday: Default::default(), + power_today: Default::default(), + power_total: Default::default(), + power_total_low: Default::default(), + power_total_high: Default::default(), + gas_total: Default::default(), + co2: Default::default(), + pms_state: Default::default(), + last_seen: Instant::now(), + firmware: Default::default(), + version: 0.0, + } + } + + pub fn update(&mut self, json: JsonValue) { + self.last_seen = Instant::now(); + + if json["DeviceName"].is_string() && !json["DeviceName"].is_empty() { + self.name = json["DeviceName"].to_string(); + } + if json["POWER"].is_string() && !json["POWER"].is_empty() { + self.state = Some(json["POWER"] == "ON"); + } + if let Some(power) = json["ENERGY"]["Power"].as_number().map(f32::from) { + self.power_watts = Some(power); + } + if let Some(yesterday) = json["ENERGY"]["Yesterday"].as_number().map(f32::from) { + self.power_yesterday = Some(yesterday); + } + if let Some(today) = json["ENERGY"]["Today"].as_number().map(f32::from) { + self.power_today = Some(today); + } + if let Some(co2) = json["MHZ19B"]["CarbonDioxide"].as_number().map(f32::from) { + if co2 > 1.0 { + self.co2 = Some(co2); + } + } + if let Some(power) = json["OBIS"]["Power"].as_number().map(f32::from) { + self.power_watts = Some(power); + } + if let Some(total) = json["OBIS"]["Total"].as_number().map(f32::from) { + self.power_total = Some(total); + } + if let Some(total) = json["OBIS"]["Total_high"].as_number().map(f32::from) { + self.power_total_high = Some(total); + } + if let Some(total) = json["OBIS"]["Total_low"].as_number().map(f32::from) { + self.power_total_low = Some(total); + } + if let Some(gas) = json["OBIS"]["Gas_total"].as_number().map(f32::from) { + self.gas_total = Some(gas); + } + + if let Some(version) = json["StatusFWR"]["Version"].as_str() { + self.firmware = version.into(); + if let Some(version) = version + .rfind('.') + .map(|index| &version[0..index]) + .and_then(|s| s.parse().ok()) + { + self.version = version + } + } + + if json["PMS5003"].is_object() { + let pms = self.pms_state.get_or_insert(PMSState::default()); + pms.update(&json["PMS5003"]); + } + } + + pub fn format_open_metrics(&self) -> impl Display + '_ { + TasmotaFormatter { device: self } + } +} + +struct TasmotaFormatter<'a> { + device: &'a DeviceState, +} + +impl Display for TasmotaFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.device.name.is_empty() { + return Ok(()); + } + writeln!( + f, + "tasmota_online{{tasmota_id=\"{}\", name=\"{}\"}} 1", + self.device.hostname, self.device.name + )?; + if let Some(switch_state) = self.device.state { + writeln!( + f, + "switch_state{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, + self.device.name, + if switch_state { 1 } else { 0 } + )?; + } + + if let Some(power_watts) = self.device.power_watts { + writeln!( + f, + "power_watts{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_watts + )?; + } + + if let Some(power_yesterday) = self.device.power_yesterday { + writeln!( + f, + "power_yesterday_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_yesterday + )?; + } + + if let Some(power_today) = self.device.power_today { + writeln!( + f, + "power_today_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_today + )?; + } + + if let Some(power_total) = self.device.power_total { + writeln!( + f, + "power_total_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_total + )?; + } + + if let Some(power_total) = self.device.power_total_high { + writeln!( + f, + "power_total_high_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_total + )?; + } + + if let Some(power_total) = self.device.power_total_low { + writeln!( + f, + "power_total_low_kwh{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, power_total + )?; + } + + if let Some(gas_total) = self.device.gas_total { + writeln!( + f, + "gas_total_m3{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, gas_total + )?; + } + + if let Some(co2) = self.device.co2 { + writeln!( + f, + "sensor_co2{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, self.device.name, co2 + )?; + } + + if let Some(pms) = self.device.pms_state.as_ref() { + write!( + f, + "{}", + PmsFormatter { + device: self.device, + pms + } + )?; + } + + if !self.device.firmware.is_empty() { + writeln!( + f, + r#"tasmota_version{{tasmota_id="{}", name="{}", firmware="{}", version="{}"}} 1"#, + self.device.hostname, self.device.name, self.device.firmware, self.device.version + )?; + } + + Ok(()) + } +} + +//"PMS5003":{"CF1":6,"CF2.5":8,"CF10":8,"PM1":6,"PM2.5":8,"PM10":8,"PB0.3":0,"PB0.5":0,"PB1":0,"PB2.5":0,"PB5":0,"PB10":0} + +#[derive(Debug, Clone, Default)] +pub struct PMSState { + cf1: u16, + cf2_5: u16, + cf10: u16, + pm1: u16, + pm2_5: u16, + pm10: u16, + pb0_3: u16, + pb0_5: u16, + pb1: u16, + pb2_5: u16, + pb5: u16, + pb10: u16, +} + +impl PMSState { + pub fn update(&mut self, json: &JsonValue) { + if let Some(val) = json["CF1"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.cf1 = val; + } + if let Some(val) = json["CF2.5"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.cf2_5 = val; + } + if let Some(val) = json["CF10"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.cf10 = val; + } + if let Some(val) = json["PM1"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pm1 = val; + } + if let Some(val) = json["PM2.5"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pm2_5 = val; + } + if let Some(val) = json["PM10"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pm10 = val; + } + if let Some(val) = json["PB0.3"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb0_3 = val; + } + if let Some(val) = json["PB0.5"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb0_5 = val; + } + if let Some(val) = json["PB1"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb1 = val; + } + if let Some(val) = json["PB2.5"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb2_5 = val; + } + if let Some(val) = json["PB5"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb5 = val; + } + if let Some(val) = json["PB10"] + .as_number() + .and_then(|num| u16::try_from(num).ok()) + { + self.pb10 = val; + } + } +} + +struct PmsFormatter<'a> { + pms: &'a PMSState, + device: &'a DeviceState, +} + +impl Display for PmsFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = &self.device.name; + + writeln!( + f, + "cf1{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.cf1 + )?; + writeln!( + f, + "cf2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.cf2_5 + )?; + writeln!( + f, + "cf10{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.cf10 + )?; + writeln!( + f, + "pm1{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pm1 + )?; + writeln!( + f, + "pm2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pm2_5 + )?; + writeln!( + f, + "pm10{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pm10 + )?; + writeln!( + f, + "pb0_3{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb0_3 + )?; + writeln!( + f, + "pb0_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb0_5 + )?; + writeln!( + f, + "pb1{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb1 + )?; + writeln!( + f, + "pb2_5{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb2_5 + )?; + writeln!( + f, + "pb5{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb5 + )?; + writeln!( + f, + "pb10{{tasmota_id=\"{}\", name=\"{}\"}} {}", + self.device.hostname, name, self.pms.pb10 + )?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 2fbd1e1..983ed51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod mqtt; mod topic; use crate::config::{Config, ListenConfig}; -use crate::device::{Device, DeviceStates, format_device_state}; +use crate::device::{Device, DeviceStates}; use crate::mqtt::mqtt_stream; use crate::topic::Topic; use clap::Parser; @@ -78,8 +78,9 @@ async fn serve(device_states: Arc>, listen: ListenConfig) { .map(move |state: Arc>| { let state = state.lock().unwrap(); let mut response = String::new(); - for (device, state) in state.devices() { - format_device_state(&mut response, device, state).unwrap(); + for (_, state) in state.devices() { + write!(&mut response, "{}", state.format_open_metrics()) + .expect("formatting failed"); } for (_, state) in state.dsmr_devices() { write!(&mut response, "{}", state.format_open_metrics())