mirror of
https://codeberg.org/icewind/taspromto.git
synced 2026-08-02 04:04:44 +02:00
forward mitemp to home assistant
This commit is contained in:
parent
71d28cf847
commit
7dff97b474
8 changed files with 211 additions and 13 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -747,6 +747,12 @@ version = "0.4.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
|
|
@ -1264,10 +1270,12 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"hostname",
|
||||
"jzon",
|
||||
"maplit",
|
||||
"pin-utils",
|
||||
"rumqttc",
|
||||
"secretfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml",
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ pin-utils = "0.1.0"
|
|||
hostname = "0.4.2"
|
||||
tokio-stream = { version = "0.1.18", features = ["net"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
secretfile = "0.1.1"
|
||||
toml = "1.1.2"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
maplit = "1.0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ and the desired name.
|
|||
MITEMP_NAMES="351234=Bedroom,352468=Living Room"
|
||||
```
|
||||
|
||||
In addition to exposing the data to prometheus, taspromto will also publish this
|
||||
data over the same MQTT connection in a home-assistant compatible format.
|
||||
|
||||
## 433Mhz temperature sensors
|
||||
|
||||
Taspromto can parse data 433Mhz temperature sensors send to MQTT by
|
||||
|
|
|
|||
27
scratch.json
Normal file
27
scratch.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"device": {
|
||||
"identifiers": ["mitemp-58:2D:34:35:F3:D4"],
|
||||
"name": "Bedroom",
|
||||
"manufacturer": "Icewind",
|
||||
"model": "Taspromto - Mitemp"
|
||||
},
|
||||
"origin": { "name": "taspromto" },
|
||||
"components": {
|
||||
"mitemp_582D3435F3D4_humidity": {
|
||||
"platform": "sensor",
|
||||
"device_class": "humidity",
|
||||
"unit_of_measurement": "%",
|
||||
"value_template": "{{ value_json.humidity}}",
|
||||
"unique_id": "mitemp_582D3435F3D4_temperature"
|
||||
},
|
||||
"mitemp_582D3435F3D4_temperature": {
|
||||
"platform": "sensor",
|
||||
"device_class": "temperature",
|
||||
"unit_of_measurement": "°C",
|
||||
"value_template": "{{ value_json.temperature}}",
|
||||
"unique_id": "mitemp_582D3435F3D4_temperature"
|
||||
}
|
||||
},
|
||||
"state_topic": "taspromto/sensor/mitemp_582D3435F3D4/state",
|
||||
"qos": 2
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
use jzon::JsonValue;
|
||||
use maplit::hashmap;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::device::{BDAddr, FormattableDevice};
|
||||
use crate::{
|
||||
device::{BDAddr, FormattableDevice},
|
||||
hass::{DiscoveryComponent, DiscoveryConfig, DiscoveryDevice, DiscoveryOrigin},
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Debug, Display},
|
||||
time::Instant,
|
||||
|
|
@ -48,6 +53,67 @@ impl MiTempDevice {
|
|||
self.dew_point = dew_point;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discovery_topic(&self) -> String {
|
||||
format!(
|
||||
"homeassistant/device/mitemp_{}/config",
|
||||
self.addr.format_digits()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mqtt_device(&self) -> DiscoveryConfig {
|
||||
let component_temp = DiscoveryComponent {
|
||||
platform: "sensor",
|
||||
device_class: "temperature",
|
||||
unit_of_measurement: "°C",
|
||||
value_template: "{{ value_json.temperature}}",
|
||||
unique_id: format!("mitemp_{}_temperature", self.addr.format_digits()),
|
||||
};
|
||||
let component_humidity = DiscoveryComponent {
|
||||
platform: "sensor",
|
||||
device_class: "humidity",
|
||||
unit_of_measurement: "%",
|
||||
value_template: "{{ value_json.humidity}}",
|
||||
unique_id: format!("mitemp_{}_humidity", self.addr.format_digits()),
|
||||
};
|
||||
|
||||
DiscoveryConfig {
|
||||
device: DiscoveryDevice {
|
||||
identifiers: vec![format!("mitemp-{}", self.addr)],
|
||||
name: self.name.clone(),
|
||||
manufacturer: "Xiaomi",
|
||||
model: "Mitemp",
|
||||
},
|
||||
origin: DiscoveryOrigin { name: "taspromto" },
|
||||
components: hashmap! {
|
||||
"temperature" => component_temp,
|
||||
"humidity" => component_humidity,
|
||||
},
|
||||
state_topic: self.state_topic(),
|
||||
qos: 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_topic(&self) -> String {
|
||||
format!(
|
||||
"taspromto/sensor/mitemp_{}/state",
|
||||
self.addr.format_digits()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mqtt_state(&self) -> String {
|
||||
#[derive(Serialize)]
|
||||
struct State {
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
}
|
||||
|
||||
serde_json::to_string(&State {
|
||||
temperature: self.temperature,
|
||||
humidity: self.humidity,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FormattableDevice for MiTempDevice {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ use std::time::Instant;
|
|||
pub use tasmota::DeviceState;
|
||||
use tokio::task::spawn;
|
||||
|
||||
use crate::hass::DiscoveryConfig;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DeviceStates {
|
||||
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
||||
|
|
@ -32,6 +34,11 @@ pub struct DeviceStates {
|
|||
active_rf_temp_id: RfDeviceId,
|
||||
}
|
||||
|
||||
pub enum HassUpdate {
|
||||
Discovery(String, DiscoveryConfig),
|
||||
Update(String, String),
|
||||
}
|
||||
|
||||
pub trait FormattableDevice {
|
||||
fn format_open_metrics(&self) -> impl Display + '_;
|
||||
|
||||
|
|
@ -85,7 +92,7 @@ impl DeviceStates {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, device: Device, json: JsonValue) {
|
||||
pub fn update<Cb: Fn(HassUpdate)>(&mut self, device: Device, json: JsonValue, hass_cb: Cb) {
|
||||
let device = self
|
||||
.devices
|
||||
.entry(device)
|
||||
|
|
@ -97,11 +104,16 @@ impl DeviceStates {
|
|||
match BDAddr::from_mi_temp_mac_part(addr) {
|
||||
Ok(addr) => {
|
||||
if let Some(name) = self.mi_temp_names.get(&addr) {
|
||||
let state = self
|
||||
.mi_temp_devices
|
||||
.entry(addr)
|
||||
.or_insert_with(|| MiTempDevice::new(addr, name.clone()));
|
||||
let state = self.mi_temp_devices.entry(addr).or_insert_with(|| {
|
||||
let device = MiTempDevice::new(addr, name.clone());
|
||||
hass_cb(HassUpdate::Discovery(
|
||||
device.discovery_topic(),
|
||||
device.mqtt_device(),
|
||||
));
|
||||
device
|
||||
});
|
||||
state.update(value);
|
||||
hass_cb(HassUpdate::Update(state.state_topic(), state.mqtt_state()));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to parse mitemp mac: {:#}", e),
|
||||
|
|
@ -261,6 +273,28 @@ impl BDAddr {
|
|||
address.reverse();
|
||||
Ok(BDAddr { address })
|
||||
}
|
||||
|
||||
/// Format the address as only the digits, leaving out the semicolons
|
||||
pub fn format_digits(&self) -> impl Display + use<'_> {
|
||||
struct DigitFormatter<'a> {
|
||||
address: &'a [u8; 6usize],
|
||||
}
|
||||
|
||||
impl Display for DigitFormatter<'_> {
|
||||
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]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DigitFormatter {
|
||||
address: &self.address,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BDAddr {
|
||||
|
|
|
|||
34
src/hass.rs
Normal file
34
src/hass.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiscoveryConfig {
|
||||
pub device: DiscoveryDevice,
|
||||
pub origin: DiscoveryOrigin,
|
||||
pub components: HashMap<&'static str, DiscoveryComponent>,
|
||||
pub state_topic: String,
|
||||
pub qos: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiscoveryDevice {
|
||||
pub identifiers: Vec<String>,
|
||||
pub name: String,
|
||||
pub manufacturer: &'static str,
|
||||
pub model: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiscoveryOrigin {
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DiscoveryComponent {
|
||||
pub platform: &'static str,
|
||||
pub device_class: &'static str,
|
||||
pub unit_of_measurement: &'static str,
|
||||
pub value_template: &'static str,
|
||||
pub unique_id: String,
|
||||
}
|
||||
38
src/main.rs
38
src/main.rs
|
|
@ -1,10 +1,11 @@
|
|||
mod config;
|
||||
mod device;
|
||||
mod hass;
|
||||
mod mqtt;
|
||||
mod topic;
|
||||
|
||||
use crate::config::{Config, ListenConfig};
|
||||
use crate::device::{Device, DeviceStates, FormattableDevice};
|
||||
use crate::device::{Device, DeviceStates, FormattableDevice, HassUpdate};
|
||||
use crate::mqtt::mqtt_stream;
|
||||
use crate::topic::Topic;
|
||||
use clap::Parser;
|
||||
|
|
@ -125,11 +126,11 @@ async fn mqtt_client<S: Stream<Item = Result<Publish>>>(
|
|||
) -> Result<()> {
|
||||
while let Some(message) = stream.next().await {
|
||||
let message = message?;
|
||||
println!(
|
||||
"{} {}",
|
||||
message.topic,
|
||||
std::str::from_utf8(message.payload.as_ref()).unwrap_or_default()
|
||||
);
|
||||
// println!(
|
||||
// "{} {}",
|
||||
// message.topic,
|
||||
// std::str::from_utf8(message.payload.as_ref()).unwrap_or_default()
|
||||
// );
|
||||
let topic = Topic::from(message.topic.as_str());
|
||||
|
||||
match topic {
|
||||
|
|
@ -150,10 +151,33 @@ async fn mqtt_client<S: Stream<Item = Result<Publish>>>(
|
|||
}
|
||||
Topic::Power(_) => {}
|
||||
Topic::Result(device) | Topic::Sensor(device) | Topic::Status(device) => {
|
||||
let send_client = client.clone();
|
||||
|
||||
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
|
||||
if let Ok(json) = jzon::parse(payload) {
|
||||
let mut device_states = device_states.lock().unwrap();
|
||||
device_states.update(device, json);
|
||||
device_states.update(device, json, |update| {
|
||||
let send_client = send_client.clone();
|
||||
spawn(async move {
|
||||
match update {
|
||||
HassUpdate::Discovery(topic, payload) => {
|
||||
send_client
|
||||
.publish(
|
||||
topic,
|
||||
QoS::AtLeastOnce,
|
||||
true,
|
||||
serde_json::to_vec(&payload).unwrap(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
HassUpdate::Update(topic, payload) => {
|
||||
send_client
|
||||
.publish(topic, QoS::AtLeastOnce, true, payload)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Topic::Msg(_device) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue