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",
|
"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]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -724,6 +735,12 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "match_cfg"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
@ -1421,6 +1438,7 @@ dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"hostname",
|
||||||
"json",
|
"json",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"rumqttc",
|
"rumqttc",
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,4 @@ ctrlc = { version = "3.1.7", features = ["termination"] }
|
||||||
color-eyre = "0.5.7"
|
color-eyre = "0.5.7"
|
||||||
async-stream = "0.3.0"
|
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;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -6,6 +8,7 @@ pub struct Config {
|
||||||
pub mqtt_host: String,
|
pub mqtt_host: String,
|
||||||
pub mqtt_port: u16,
|
pub mqtt_port: u16,
|
||||||
pub host_port: u16,
|
pub host_port: u16,
|
||||||
|
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -20,10 +23,28 @@ impl Config {
|
||||||
.and_then(|port| u16::from_str(&port).ok())
|
.and_then(|port| u16::from_str(&port).ok())
|
||||||
.unwrap_or(80);
|
.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 {
|
Ok(Config {
|
||||||
mqtt_host,
|
mqtt_host,
|
||||||
mqtt_port,
|
mqtt_port,
|
||||||
host_port,
|
host_port,
|
||||||
|
mi_temp_names,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
src/device.rs
159
src/device.rs
|
|
@ -1,6 +1,8 @@
|
||||||
|
use color_eyre::{eyre::WrapErr, Report, Result};
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::Write;
|
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub struct Device {
|
pub struct Device {
|
||||||
|
|
@ -16,11 +18,12 @@ impl Device {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct DeviceState {
|
pub struct DeviceState {
|
||||||
pub state: bool,
|
pub state: Option<bool>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub power_watts: Option<f32>,
|
pub power_watts: Option<f32>,
|
||||||
pub power_yesterday: Option<f32>,
|
pub power_yesterday: Option<f32>,
|
||||||
pub power_today: Option<f32>,
|
pub power_today: Option<f32>,
|
||||||
|
pub mi_temp_devices: BTreeMap<BDAddr, MiTempState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceState {
|
impl DeviceState {
|
||||||
|
|
@ -28,6 +31,9 @@ impl DeviceState {
|
||||||
if json["DeviceName"].is_string() && !json["DeviceName"].is_empty() {
|
if json["DeviceName"].is_string() && !json["DeviceName"].is_empty() {
|
||||||
self.name = json["DeviceName"].to_string();
|
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"]
|
if let Some(power) = json["ENERGY"]["Power"]
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|num| f32::try_from(num).ok())
|
.and_then(|num| f32::try_from(num).ok())
|
||||||
|
|
@ -46,6 +52,55 @@ impl DeviceState {
|
||||||
{
|
{
|
||||||
self.power_today = Some(today);
|
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,
|
mut writer: W,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
state: &DeviceState,
|
state: &DeviceState,
|
||||||
) -> Result<(), std::fmt::Error> {
|
mi_temp_names: &BTreeMap<BDAddr, String>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
if let Some(switch_state) = state.state {
|
||||||
writeln!(
|
writeln!(
|
||||||
writer,
|
writer,
|
||||||
"switch_state{{tasmota_id=\"{}\", name=\"{}\"}} {}",
|
"switch_state{{tasmota_id=\"{}\", name=\"{}\"}} {}",
|
||||||
device.hostname,
|
device.hostname,
|
||||||
state.name,
|
state.name,
|
||||||
if state.state { 1 } else { 0 }
|
if switch_state { 1 } else { 0 }
|
||||||
)?;
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(power_watts) = state.power_watts {
|
if let Some(power_watts) = state.power_watts {
|
||||||
writeln!(
|
writeln!(
|
||||||
|
|
@ -85,5 +143,98 @@ pub fn format_device_state<W: Write>(
|
||||||
device.hostname, state.name, power_today
|
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(())
|
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::device::{format_device_state, Device, DeviceState};
|
||||||
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, Report, Result};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use pin_utils::pin_mut;
|
use pin_utils::pin_mut;
|
||||||
use rumqttc::{MqttOptions, QoS};
|
use rumqttc::{MqttOptions, QoS};
|
||||||
|
|
@ -31,9 +31,11 @@ async fn main() -> Result<()> {
|
||||||
.expect("Error setting Ctrl-C handler");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
let states = device_states.clone();
|
let states = device_states.clone();
|
||||||
|
let mqtt_host = config.mqtt_host;
|
||||||
|
let mqtt_port = config.mqtt_port;
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
loop {
|
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!("lost mqtt collection: {:#}", e);
|
||||||
}
|
}
|
||||||
eprintln!("reconnecting after 1s");
|
eprintln!("reconnecting after 1s");
|
||||||
|
|
@ -42,12 +44,20 @@ async fn main() -> Result<()> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = warp::any().map(move || device_states.clone());
|
let state = warp::any().map(move || device_states.clone());
|
||||||
|
|
||||||
|
let mi_temp_names = config.mi_temp_names;
|
||||||
let metrics = warp::path!("metrics")
|
let metrics = warp::path!("metrics")
|
||||||
.and(state)
|
.and(state)
|
||||||
.map(|state: DeviceStates| {
|
.map(move |state: DeviceStates| {
|
||||||
let mut response = String::new();
|
let mut response = String::new();
|
||||||
for device in state.iter() {
|
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
|
response
|
||||||
});
|
});
|
||||||
|
|
@ -57,7 +67,10 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mqtt_client(host: &str, port: u16, device_states: DeviceStates) -> 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);
|
mqtt_options.set_keep_alive(5);
|
||||||
|
|
||||||
let (client, stream) = mqtt_stream(mqtt_options)
|
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) => {
|
Topic::Power(_) => {}
|
||||||
let state = message.payload.as_ref() == b"ON";
|
|
||||||
device_states.entry(device).or_default().state = state;
|
|
||||||
}
|
|
||||||
Topic::Result(device) => {
|
Topic::Result(device) => {
|
||||||
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
|
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
|
||||||
dbg!(payload);
|
|
||||||
if let Ok(json) = json::parse(payload) {
|
if let Ok(json) = json::parse(payload) {
|
||||||
let mut device_state = device_states.entry(device).or_default();
|
let mut device_state = device_states.entry(device).or_default();
|
||||||
device_state.update(json);
|
device_state.update(json);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue