mirror of
https://codeberg.org/icewind/taspromto.git
synced 2026-06-04 00:54:13 +02:00
add rflink32 support
This commit is contained in:
parent
f472cc54ef
commit
a5bb4b1d80
5 changed files with 140 additions and 1 deletions
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -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);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue