add rflink32 support

This commit is contained in:
Robin Appelman 2024-05-07 22:37:20 +02:00
commit a5bb4b1d80
5 changed files with 140 additions and 1 deletions

View file

@ -16,6 +16,12 @@ in {
description = "Names for mitemp sensors"; description = "Names for mitemp sensors";
}; };
rfChannelNames = mkOption {
type = types.attrs;
default = {};
description = "Names for 433mhz temperature sensors";
};
port = mkOption { port = mkOption {
type = types.int; type = types.int;
default = 3030; default = 3030;
@ -40,6 +46,7 @@ in {
environment = { environment = {
PORT = toString cfg.port; PORT = toString cfg.port;
MITEMP_NAMES = concatStringsSep "," (map (k: k + "=" + cfg.mitempNames."${k}") (attrNames cfg.mitempNames)); MITEMP_NAMES = concatStringsSep "," (map (k: k + "=" + cfg.mitempNames."${k}") (attrNames cfg.mitempNames));
RF_TEMP_NAMES = concatStringsSep "," (map (k: k + "=" + cfg.rfChannelNames."${k}") (attrNames cfg.rfChannelNames));
}; };
serviceConfig = { serviceConfig = {

View file

@ -11,6 +11,7 @@ pub struct Config {
pub mqtt_port: u16, pub mqtt_port: u16,
pub host_port: u16, pub host_port: u16,
pub mi_temp_names: BTreeMap<BDAddr, String>, pub mi_temp_names: BTreeMap<BDAddr, String>,
pub rf_temp_names: BTreeMap<u8, String>,
pub mqtt_credentials: Option<Credentials>, pub mqtt_credentials: Option<Credentials>,
} }
@ -48,6 +49,20 @@ impl Config {
}) })
.collect::<Result<BTreeMap<BDAddr, String>, Report>>()?; .collect::<Result<BTreeMap<BDAddr, String>, Report>>()?;
let rf_temp_names = dotenvy::var("RF_TEMP_NAMES").unwrap_or_default();
let rf_temp_names = rf_temp_names
.split(',')
.map(|pair| {
let mut parts = pair.split('=');
if let (Some(mac), Some(name)) = (parts.next().map(u8::from_str), parts.next()) {
let channel = mac.wrap_err("Invalid RF_TEMP_NAMES")?;
Ok((channel, name.to_string()))
} else {
Err(Report::msg("Invalid RF_TEMP_NAMES"))
}
})
.collect::<Result<BTreeMap<u8, String>, Report>>()?;
let mqtt_credentials = match dotenvy::var("MQTT_USERNAME") { let mqtt_credentials = match dotenvy::var("MQTT_USERNAME") {
Ok(username) => { Ok(username) => {
let password = dotenvy::var("MQTT_PASSWORD") let password = dotenvy::var("MQTT_PASSWORD")
@ -62,6 +77,7 @@ impl Config {
mqtt_port, mqtt_port,
host_port, host_port,
mi_temp_names, mi_temp_names,
rf_temp_names,
mqtt_credentials, mqtt_credentials,
}) })
} }

View file

@ -11,6 +11,7 @@ use tokio::task::spawn;
pub struct DeviceStates { pub struct DeviceStates {
pub devices: HashMap<Device, DeviceState>, pub devices: HashMap<Device, DeviceState>,
pub mi_temp_devices: BTreeMap<BDAddr, MiTempState>, pub mi_temp_devices: BTreeMap<BDAddr, MiTempState>,
pub rf_temp_devices: BTreeMap<u8, TempState>,
} }
impl DeviceStates { impl DeviceStates {
@ -36,11 +37,26 @@ impl DeviceStates {
device.update(json); device.update(json);
} }
pub fn update_rf(&mut self, payload: &str) {
if let Some(data) = parse_rf_payload(payload) {
let state = self.rf_temp_devices.entry(data.channel).or_default();
state.humidity = data.humidity;
state.temperature = data.temperature;
} else {
eprintln!("invalid rf payload: {payload}")
}
}
pub fn mi_temp(&self) -> impl Iterator<Item = (&BDAddr, &MiTempState)> { pub fn mi_temp(&self) -> impl Iterator<Item = (&BDAddr, &MiTempState)> {
self.mi_temp_devices.iter() self.mi_temp_devices.iter()
} }
pub fn rf_temp(&self) -> impl Iterator<Item = (u8, &TempState)> {
self.rf_temp_devices
.iter()
.map(|(channel, state)| (*channel, state))
}
pub fn retain(&mut self, cleanup_time: Instant, ping_time: Instant, client: &AsyncClient) { pub fn retain(&mut self, cleanup_time: Instant, ping_time: Instant, client: &AsyncClient) {
self.devices.retain(|device, state| { self.devices.retain(|device, state| {
if state.last_seen < cleanup_time { if state.last_seen < cleanup_time {
@ -406,6 +422,42 @@ pub fn format_mi_temp_state<W: Write>(
Ok(()) Ok(())
} }
#[derive(Debug, Default)]
pub struct TempState {
temperature: f32,
humidity: u8,
}
pub fn format_rf_temp_state<W: Write>(
mut writer: W,
channel: u8,
names: &BTreeMap<u8, String>,
state: &TempState,
) -> std::fmt::Result {
let name = if let Some(name) = names.get(&channel) {
name
} else {
return Ok(());
};
if state.temperature > 0.0 {
writeln!(
writer,
"sensor_temperature{{channel=\"{}\", name=\"{}\"}} {}",
channel, name, state.temperature
)?;
}
if state.humidity > 0 {
writeln!(
writer,
"sensor_humidity{{channel=\"{}\", name=\"{}\"}} {}",
channel, name, state.humidity
)?;
}
Ok(())
}
/// Stores the 6 byte address used to identify Bluetooth devices. /// Stores the 6 byte address used to identify Bluetooth devices.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Hash, Eq, PartialEq, Default, Ord, PartialOrd)] #[derive(Copy, Clone, Hash, Eq, PartialEq, Default, Ord, PartialOrd)]
@ -617,3 +669,48 @@ 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,
}
fn parse_rf_payload(payload: &str) -> Option<RfPayload> {
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()
)
}

View file

@ -4,7 +4,9 @@ mod mqtt;
mod topic; mod topic;
use crate::config::Config; use crate::config::Config;
use crate::device::{format_device_state, format_mi_temp_state, Device, DeviceStates}; use crate::device::{
format_device_state, format_mi_temp_state, format_rf_temp_state, Device, DeviceStates,
};
use crate::mqtt::mqtt_stream; use crate::mqtt::mqtt_stream;
use crate::topic::Topic; use crate::topic::Topic;
use color_eyre::{eyre::WrapErr, Result}; use color_eyre::{eyre::WrapErr, Result};
@ -56,6 +58,7 @@ async fn main() -> Result<()> {
async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) { async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
let host_port = config.host_port; let host_port = config.host_port;
let mi_temp_names = config.mi_temp_names.clone(); let mi_temp_names = config.mi_temp_names.clone();
let rf_temp_names = config.rf_temp_names.clone();
let state = warp::any().map(move || device_states.clone()); let state = warp::any().map(move || device_states.clone());
@ -70,6 +73,9 @@ async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
for (addr, state) in state.mi_temp() { for (addr, state) in state.mi_temp() {
format_mi_temp_state(&mut response, *addr, &mi_temp_names, state).unwrap() format_mi_temp_state(&mut response, *addr, &mi_temp_names, state).unwrap()
} }
for (channel, state) in state.rf_temp() {
format_rf_temp_state(&mut response, channel, &rf_temp_names, state).unwrap()
}
response response
}); });
@ -126,6 +132,11 @@ async fn mqtt_client<S: Stream<Item = Result<Publish>>>(
device_states.update(device, json); device_states.update(device, json);
} }
} }
Topic::Msg(_device) => {
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
let mut device_states = device_states.lock().unwrap();
device_states.update_rf(payload);
}
_ => {} _ => {}
} }
} }

View file

@ -9,10 +9,18 @@ pub enum Topic {
Result(Device), Result(Device),
Other(String), Other(String),
Status(Device), Status(Device),
Msg(Device),
} }
impl From<&str> for Topic { impl From<&str> for Topic {
fn from(raw: &str) -> Self { fn from(raw: &str) -> Self {
if let Some(rf_name) = raw.strip_suffix("/msg") {
let device = Device {
hostname: rf_name.to_string(),
};
return Topic::Msg(device);
}
let mut parts = raw.split('/'); let mut parts = raw.split('/');
if let (Some(prefix), Some(hostname), Some(cmd)) = if let (Some(prefix), Some(hostname), Some(cmd)) =
(parts.next(), parts.next(), parts.next()) (parts.next(), parts.next(), parts.next())