mirror of
https://codeberg.org/icewind/taspromto.git
synced 2026-06-03 16:44:11 +02:00
allow passing trough mitemp sensors
This commit is contained in:
parent
0acb41b347
commit
f7938d1d95
5 changed files with 222 additions and 22 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -557,6 +557,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.1"
|
||||
|
|
@ -724,6 +735,12 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.8"
|
||||
|
|
@ -1421,6 +1438,7 @@ dependencies = [
|
|||
"ctrlc",
|
||||
"dashmap",
|
||||
"dotenv",
|
||||
"hostname",
|
||||
"json",
|
||||
"pin-utils",
|
||||
"rumqttc",
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ dotenv = "0.15.0"
|
|||
ctrlc = { version = "3.1.7", features = ["termination"] }
|
||||
color-eyre = "0.5.7"
|
||||
async-stream = "0.3.0"
|
||||
pin-utils = "0.1.0"
|
||||
pin-utils = "0.1.0"
|
||||
hostname = "^0.3"
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use crate::device::BDAddr;
|
||||
use color_eyre::{eyre::WrapErr, Report, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -6,6 +8,7 @@ pub struct Config {
|
|||
pub mqtt_host: String,
|
||||
pub mqtt_port: u16,
|
||||
pub host_port: u16,
|
||||
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -20,10 +23,28 @@ impl Config {
|
|||
.and_then(|port| u16::from_str(&port).ok())
|
||||
.unwrap_or(80);
|
||||
|
||||
let mi_temp_names = dotenv::var("MITEMP_NAMES").unwrap_or_default();
|
||||
let mi_temp_names = mi_temp_names
|
||||
.split(',')
|
||||
.map(|pair| {
|
||||
let mut parts = pair.split('=');
|
||||
if let (Some(mac), Some(name)) = (
|
||||
parts.next().map(BDAddr::from_mi_temp_mac_part),
|
||||
parts.next(),
|
||||
) {
|
||||
let mac = mac.wrap_err("Invalid MITEMP_NAMES")?;
|
||||
Ok((mac, name.to_string()))
|
||||
} else {
|
||||
Err(Report::msg("Invalid MITEMP_NAMES"))
|
||||
}
|
||||
})
|
||||
.collect::<Result<BTreeMap<BDAddr, String>, Report>>()?;
|
||||
|
||||
Ok(Config {
|
||||
mqtt_host,
|
||||
mqtt_port,
|
||||
host_port,
|
||||
mi_temp_names,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
171
src/device.rs
171
src/device.rs
|
|
@ -1,6 +1,8 @@
|
|||
use color_eyre::{eyre::WrapErr, Report, Result};
|
||||
use json::JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub struct Device {
|
||||
|
|
@ -16,11 +18,12 @@ impl Device {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DeviceState {
|
||||
pub state: bool,
|
||||
pub state: Option<bool>,
|
||||
pub name: String,
|
||||
pub power_watts: Option<f32>,
|
||||
pub power_yesterday: Option<f32>,
|
||||
pub power_today: Option<f32>,
|
||||
pub mi_temp_devices: BTreeMap<BDAddr, MiTempState>,
|
||||
}
|
||||
|
||||
impl DeviceState {
|
||||
|
|
@ -28,6 +31,9 @@ impl DeviceState {
|
|||
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()
|
||||
.and_then(|num| f32::try_from(num).ok())
|
||||
|
|
@ -46,6 +52,55 @@ impl DeviceState {
|
|||
{
|
||||
self.power_today = Some(today);
|
||||
}
|
||||
|
||||
for (key, value) in json.entries() {
|
||||
if let Some(addr) = key.strip_prefix("MJ_HT_V1-") {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MiTempState {
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
dew_point: f32,
|
||||
battery: u8,
|
||||
}
|
||||
|
||||
impl MiTempState {
|
||||
pub fn update(&mut self, json: &JsonValue) {
|
||||
if let Some(temperature) = json["Temperature"]
|
||||
.as_number()
|
||||
.and_then(|num| f32::try_from(num).ok())
|
||||
{
|
||||
self.temperature = temperature;
|
||||
}
|
||||
if let Some(humidity) = json["Humidity"]
|
||||
.as_number()
|
||||
.and_then(|num| f32::try_from(num).ok())
|
||||
{
|
||||
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()
|
||||
.and_then(|num| f32::try_from(num).ok())
|
||||
{
|
||||
self.dew_point = dew_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,14 +108,17 @@ pub fn format_device_state<W: Write>(
|
|||
mut writer: W,
|
||||
device: &Device,
|
||||
state: &DeviceState,
|
||||
) -> Result<(), std::fmt::Error> {
|
||||
writeln!(
|
||||
writer,
|
||||
"switch_state{{tasmota_id=\"{}\", name=\"{}\"}} {}",
|
||||
device.hostname,
|
||||
state.name,
|
||||
if state.state { 1 } else { 0 }
|
||||
)?;
|
||||
mi_temp_names: &BTreeMap<BDAddr, String>,
|
||||
) -> std::fmt::Result {
|
||||
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!(
|
||||
|
|
@ -85,5 +143,98 @@ pub fn format_device_state<W: Write>(
|
|||
device.hostname, state.name, power_today
|
||||
)?;
|
||||
}
|
||||
|
||||
for (addr, state) in state.mi_temp_devices.iter() {
|
||||
format_mi_temp_state(&mut writer, *addr, mi_temp_names, state)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn format_mi_temp_state<W: Write>(
|
||||
mut writer: W,
|
||||
addr: BDAddr,
|
||||
names: &BTreeMap<BDAddr, String>,
|
||||
state: &MiTempState,
|
||||
) -> std::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(())
|
||||
}
|
||||
|
||||
/// Stores the 6 byte address used to identify Bluetooth devices.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Copy, Clone, Hash, Eq, PartialEq, Default, Ord, PartialOrd)]
|
||||
#[repr(C)]
|
||||
pub struct BDAddr {
|
||||
pub address: [u8; 6usize],
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/main.rs
29
src/main.rs
|
|
@ -7,7 +7,7 @@ use crate::config::Config;
|
|||
use crate::device::{format_device_state, Device, DeviceState};
|
||||
use crate::mqtt::mqtt_stream;
|
||||
use crate::topic::Topic;
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use color_eyre::{eyre::WrapErr, Report, Result};
|
||||
use dashmap::DashMap;
|
||||
use pin_utils::pin_mut;
|
||||
use rumqttc::{MqttOptions, QoS};
|
||||
|
|
@ -31,9 +31,11 @@ async fn main() -> Result<()> {
|
|||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let states = device_states.clone();
|
||||
let mqtt_host = config.mqtt_host;
|
||||
let mqtt_port = config.mqtt_port;
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
if let Err(e) = mqtt_client(&config.mqtt_host, config.mqtt_port, states.clone()).await {
|
||||
if let Err(e) = mqtt_client(&mqtt_host, mqtt_port, states.clone()).await {
|
||||
eprintln!("lost mqtt collection: {:#}", e);
|
||||
}
|
||||
eprintln!("reconnecting after 1s");
|
||||
|
|
@ -42,12 +44,20 @@ async fn main() -> Result<()> {
|
|||
});
|
||||
|
||||
let state = warp::any().map(move || device_states.clone());
|
||||
|
||||
let mi_temp_names = config.mi_temp_names;
|
||||
let metrics = warp::path!("metrics")
|
||||
.and(state)
|
||||
.map(|state: DeviceStates| {
|
||||
.map(move |state: DeviceStates| {
|
||||
let mut response = String::new();
|
||||
for device in state.iter() {
|
||||
format_device_state(&mut response, &device.key(), &device.value()).unwrap();
|
||||
format_device_state(
|
||||
&mut response,
|
||||
&device.key(),
|
||||
&device.value(),
|
||||
&mi_temp_names,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
response
|
||||
});
|
||||
|
|
@ -57,7 +67,10 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
async fn mqtt_client(host: &str, port: u16, device_states: DeviceStates) -> Result<()> {
|
||||
let mut mqtt_options = MqttOptions::new("taspromto", host, port);
|
||||
let hostname = hostname::get()?
|
||||
.into_string()
|
||||
.map_err(|_| Report::msg("invalid hostname"))?;
|
||||
let mut mqtt_options = MqttOptions::new(format!("taspromto-{}", hostname), host, port);
|
||||
mqtt_options.set_keep_alive(5);
|
||||
|
||||
let (client, stream) = mqtt_stream(mqtt_options)
|
||||
|
|
@ -104,13 +117,9 @@ async fn mqtt_client(host: &str, port: u16, device_states: DeviceStates) -> Resu
|
|||
}
|
||||
});
|
||||
}
|
||||
Topic::Power(device) => {
|
||||
let state = message.payload.as_ref() == b"ON";
|
||||
device_states.entry(device).or_default().state = state;
|
||||
}
|
||||
Topic::Power(_) => {}
|
||||
Topic::Result(device) => {
|
||||
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
|
||||
dbg!(payload);
|
||||
if let Ok(json) = json::parse(payload) {
|
||||
let mut device_state = device_states.entry(device).or_default();
|
||||
device_state.update(json);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue