mirror of
https://codeberg.org/icewind/taspromto.git
synced 2026-08-02 12:14:45 +02:00
start splitting device module
This commit is contained in:
parent
ccbac033d3
commit
2936b3cf22
2 changed files with 95 additions and 84 deletions
92
src/device/mitemp.rs
Normal file
92
src/device/mitemp.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use jzon::JsonValue;
|
||||
|
||||
use crate::device::BDAddr;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Debug, Write},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MiTempState {
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
dew_point: f32,
|
||||
battery: u8,
|
||||
pub last_seen: Instant,
|
||||
}
|
||||
|
||||
impl Default for MiTempState {
|
||||
fn default() -> Self {
|
||||
MiTempState {
|
||||
temperature: 0.0,
|
||||
humidity: 0.0,
|
||||
dew_point: 0.0,
|
||||
battery: 0,
|
||||
last_seen: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MiTempState {
|
||||
pub fn update(&mut self, json: &JsonValue) {
|
||||
self.last_seen = Instant::now();
|
||||
if let Some(temperature) = json["Temperature"].as_number().map(f32::from) {
|
||||
self.temperature = temperature;
|
||||
}
|
||||
if let Some(humidity) = json["Humidity"].as_number().map(f32::from) {
|
||||
self.humidity = humidity;
|
||||
}
|
||||
if let Some(battery) = json["Battery"]
|
||||
.as_number()
|
||||
.and_then(|num| u8::try_from(num).ok())
|
||||
{
|
||||
self.battery = battery;
|
||||
}
|
||||
if let Some(dew_point) = json["DewPoint"].as_number().map(f32::from) {
|
||||
self.dew_point = dew_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_mi_temp_state<W: Write>(
|
||||
mut writer: W,
|
||||
addr: BDAddr,
|
||||
names: &BTreeMap<BDAddr, String>,
|
||||
state: &MiTempState,
|
||||
) -> fmt::Result {
|
||||
// sensor_battery{name="Living Room", mac="58:2D:34:39:1D:5B"} 100
|
||||
// sensor_temperature{name="Living Room", mac="58:2D:34:39:1D:5B"} 16.2
|
||||
// sensor_humidity{name="Living Room", mac="58:2D:34:39:1D:5B"} 61.
|
||||
|
||||
let name = if let Some(name) = names.get(&addr) {
|
||||
name
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if state.battery > 0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"sensor_battery{{mac=\"{}\", name=\"{}\"}} {}",
|
||||
addr, name, state.battery
|
||||
)?;
|
||||
}
|
||||
|
||||
if state.temperature > 0.0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"sensor_temperature{{mac=\"{}\", name=\"{}\"}} {}",
|
||||
addr, name, state.temperature
|
||||
)?;
|
||||
}
|
||||
|
||||
if state.humidity > 0.0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"sensor_humidity{{mac=\"{}\", name=\"{}\"}} {}",
|
||||
addr, name, state.humidity
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
785
src/device/mod.rs
Normal file
785
src/device/mod.rs
Normal file
|
|
@ -0,0 +1,785 @@
|
|||
mod mitemp;
|
||||
|
||||
pub use crate::device::mitemp::{MiTempState, format_mi_temp_state};
|
||||
use color_eyre::{Report, Result, eyre::WrapErr};
|
||||
use jzon::JsonValue;
|
||||
use rumqttc::{AsyncClient, QoS};
|
||||
use serde::de::Error;
|
||||
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::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
use tokio::task::spawn;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DeviceStates {
|
||||
pub devices: HashMap<Device, DeviceState>,
|
||||
pub dsmr_devices: HashMap<Device, DsmrState>,
|
||||
pub mi_temp_devices: BTreeMap<BDAddr, MiTempState>,
|
||||
pub rf_temp_devices: HashMap<RfDeviceId, TempState>,
|
||||
active_rf_temp_id: RfDeviceId,
|
||||
}
|
||||
|
||||
impl DeviceStates {
|
||||
pub fn devices(&self) -> impl Iterator<Item = (&Device, &DeviceState)> {
|
||||
self.devices.iter()
|
||||
}
|
||||
|
||||
pub fn dsmr_devices(&self) -> impl Iterator<Item = (&Device, &DsmrState)> {
|
||||
self.dsmr_devices.iter()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, device: Device, json: JsonValue) {
|
||||
let device = self.devices.entry(device).or_default();
|
||||
|
||||
for (key, value) in json.entries() {
|
||||
if let Some(addr) = key.strip_prefix("MJ_HT_V1") {
|
||||
let addr = addr.trim_start_matches('-');
|
||||
match BDAddr::from_mi_temp_mac_part(addr) {
|
||||
Ok(addr) => {
|
||||
let state = self.mi_temp_devices.entry(addr).or_default();
|
||||
state.update(value);
|
||||
}
|
||||
Err(e) => eprintln!("Failed to parse mitemp mac: {:#}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device.update(json);
|
||||
}
|
||||
|
||||
pub fn update_dsmr(&mut self, device: Device, ty: DsmrMessageType, payload: &str) {
|
||||
if let Ok(value) = payload.parse() {
|
||||
let state = self.dsmr_devices.entry(device).or_default();
|
||||
match ty {
|
||||
DsmrMessageType::Water => state.water_total = Some(value),
|
||||
DsmrMessageType::Gas => state.gas_total = Some(value),
|
||||
DsmrMessageType::Energy1 => state.power_total_tariff_1 = Some(value),
|
||||
DsmrMessageType::Energy2 => state.power_total_tariff_2 = Some(value),
|
||||
DsmrMessageType::Power => state.power = Some(value),
|
||||
}
|
||||
state.last_seen = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_rf(&mut self, payload: &str) {
|
||||
if let Some(data) = parse_rf_payload(payload) {
|
||||
let state = self.rf_temp_devices.entry(data.device_id()).or_default();
|
||||
state.humidity = data.humidity;
|
||||
state.temperature = data.temperature;
|
||||
} else {
|
||||
eprintln!("invalid rf payload: {payload}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_rtl(&mut self, device: &str, field: &str, payload: &str) {
|
||||
if self.active_rf_temp_id.name != device {
|
||||
self.active_rf_temp_id = RfDeviceId::default();
|
||||
self.active_rf_temp_id.name = device.into();
|
||||
}
|
||||
match field {
|
||||
"id" => self.active_rf_temp_id.id = payload.parse().unwrap_or_default(),
|
||||
"channel" => self.active_rf_temp_id.channel = payload.parse().unwrap_or_default(),
|
||||
"temperature_F" | "temperature_C" | "humidity" => {
|
||||
self.update_active_rtl(field, payload)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_rtl(&mut self, field: &str, payload: &str) {
|
||||
let state = self
|
||||
.rf_temp_devices
|
||||
.entry(self.active_rf_temp_id.clone())
|
||||
.or_default();
|
||||
match field {
|
||||
"temperature_C" => state.temperature = payload.parse().unwrap_or_default(),
|
||||
"temperature_F" => {
|
||||
state.temperature = payload
|
||||
.parse()
|
||||
.map(|temp_f: f32| (temp_f - 32.0) * 5.0 / 9.0)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
"humidity" => state.humidity = payload.parse().unwrap_or_default(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mi_temp(&self) -> impl Iterator<Item = (&BDAddr, &MiTempState)> {
|
||||
self.mi_temp_devices.iter()
|
||||
}
|
||||
|
||||
pub fn rf_temp(&self) -> impl Iterator<Item = (&RfDeviceId, &TempState)> {
|
||||
self.rf_temp_devices.iter()
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, cleanup_time: Instant, ping_time: Instant, client: &AsyncClient) {
|
||||
self.devices.retain(|device, state| {
|
||||
if state.last_seen < cleanup_time {
|
||||
println!("{} hasn't been seen for 15m, removing", device.hostname);
|
||||
false
|
||||
} else if state.last_seen < ping_time || state.name.is_empty() {
|
||||
println!(
|
||||
"{} hasn't been seen for 10m or has no name set, pinging",
|
||||
device.hostname
|
||||
);
|
||||
let send_client = client.clone();
|
||||
let topic = device.get_topic("cmnd", "DeviceName");
|
||||
spawn(async move {
|
||||
if let Err(e) = send_client.publish(topic, QoS::AtMostOnce, false, "").await {
|
||||
eprintln!("Failed to ping device: {:#}", e);
|
||||
}
|
||||
});
|
||||
true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
self.mi_temp_devices.retain(|device, state| {
|
||||
if state.last_seen < cleanup_time {
|
||||
println!("{} hasn't been seen for 15m, removing", device);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub struct Device {
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn get_topic(&self, prefix: &str, command: &str) -> String {
|
||||
format!("{}/{}/{}", prefix, self.hostname, command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceState {
|
||||
pub state: Option<bool>,
|
||||
pub name: String,
|
||||
pub power_watts: Option<f32>,
|
||||
pub power_yesterday: Option<f32>,
|
||||
pub power_today: Option<f32>,
|
||||
pub power_total: Option<f32>,
|
||||
pub power_total_low: Option<f32>,
|
||||
pub power_total_high: Option<f32>,
|
||||
pub gas_total: Option<f32>,
|
||||
pub co2: Option<f32>,
|
||||
pub pms_state: Option<PMSState>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DsmrMessageType {
|
||||
Water,
|
||||
Gas,
|
||||
Energy1,
|
||||
Energy2,
|
||||
Power,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DsmrState {
|
||||
pub power: Option<f32>,
|
||||
pub power_total_tariff_1: Option<f32>,
|
||||
pub power_total_tariff_2: Option<f32>,
|
||||
pub gas_total: Option<f32>,
|
||||
pub water_total: Option<f32>,
|
||||
pub last_seen: Instant,
|
||||
}
|
||||
|
||||
impl Default for DsmrState {
|
||||
fn default() -> Self {
|
||||
DsmrState {
|
||||
power: None,
|
||||
power_total_tariff_1: None,
|
||||
power_total_tariff_2: None,
|
||||
gas_total: None,
|
||||
water_total: None,
|
||||
last_seen: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<W: Write>(
|
||||
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(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TempState {
|
||||
temperature: f32,
|
||||
humidity: u8,
|
||||
}
|
||||
|
||||
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();
|
||||
if power_total > 0.0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"power_total_kwh{{name=\"{}\"}} {}",
|
||||
device, power_total
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(power) = state.power_total_tariff_1 {
|
||||
writeln!(
|
||||
writer,
|
||||
"power_total_low_kwh{{name=\"{}\"}} {}",
|
||||
device, power
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(power) = state.power_total_tariff_2 {
|
||||
writeln!(
|
||||
writer,
|
||||
"power_total_high_kwh{{name=\"{}\"}} {}",
|
||||
device, power
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(power) = state.power {
|
||||
writeln!(
|
||||
writer,
|
||||
"power_watts{{name=\"{}\"}} {}",
|
||||
device,
|
||||
power * 1000.0
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(gas) = state.gas_total {
|
||||
writeln!(writer, "gas_total_m3{{name=\"{}\"}} {}", device, gas)?;
|
||||
}
|
||||
|
||||
if let Some(water) = state.water_total {
|
||||
writeln!(writer, "water_total_m3{{name=\"{}\"}} {}", device, water)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores the 6 byte address used to identify Bluetooth devices.
|
||||
#[derive(Copy, Clone, Hash, Eq, PartialEq, Default, Ord, PartialOrd)]
|
||||
#[repr(C)]
|
||||
pub struct BDAddr {
|
||||
pub address: [u8; 6usize],
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BDAddr {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let str = <Cow<'de, str>>::deserialize(deserializer)?;
|
||||
Self::from_mi_temp_mac_part(&str).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl BDAddr {
|
||||
/// parse BDAddr from the last 6 characters of the mac address
|
||||
/// first 6 characters are always set to 582D34
|
||||
pub fn from_mi_temp_mac_part(part: &str) -> Result<Self> {
|
||||
let bytes = ["58".as_bytes(), "2D".as_bytes(), "34".as_bytes()]
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(part.as_bytes().chunks_exact(2))
|
||||
.map(|part: &[u8]| {
|
||||
let part = std::str::from_utf8(part)
|
||||
.map_err(|_| Report::msg("Invalid mac address digit"))?;
|
||||
u8::from_str_radix(part, 16).wrap_err("Invalid mac address digit")
|
||||
})
|
||||
.collect::<Result<Vec<u8>>>()?;
|
||||
let mut address =
|
||||
<[u8; 6]>::try_from(bytes.as_slice()).wrap_err("Invalid mac address digit count")?;
|
||||
address.reverse();
|
||||
Ok(BDAddr { address })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BDAddr {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let a = self.address;
|
||||
write!(
|
||||
f,
|
||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
a[5], a[4], a[3], a[2], a[1], a[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for BDAddr {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
(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<W: Write>(
|
||||
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(())
|
||||
}
|
||||
|
||||
#[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()
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue