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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maplit"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
|
|
@ -1264,10 +1270,12 @@ dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"hostname",
|
"hostname",
|
||||||
"jzon",
|
"jzon",
|
||||||
|
"maplit",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"rumqttc",
|
"rumqttc",
|
||||||
"secretfile",
|
"secretfile",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ pin-utils = "0.1.0"
|
||||||
hostname = "0.4.2"
|
hostname = "0.4.2"
|
||||||
tokio-stream = { version = "0.1.18", features = ["net"] }
|
tokio-stream = { version = "0.1.18", features = ["net"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.150"
|
||||||
secretfile = "0.1.1"
|
secretfile = "0.1.1"
|
||||||
toml = "1.1.2"
|
toml = "1.1.2"
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
maplit = "1.0.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,9 @@ and the desired name.
|
||||||
MITEMP_NAMES="351234=Bedroom,352468=Living Room"
|
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
|
## 433Mhz temperature sensors
|
||||||
|
|
||||||
Taspromto can parse data 433Mhz temperature sensors send to MQTT by
|
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 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::{
|
use std::{
|
||||||
fmt::{self, Debug, Display},
|
fmt::{self, Debug, Display},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
|
|
@ -48,6 +53,67 @@ impl MiTempDevice {
|
||||||
self.dew_point = dew_point;
|
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 {
|
impl FormattableDevice for MiTempDevice {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ use std::time::Instant;
|
||||||
pub use tasmota::DeviceState;
|
pub use tasmota::DeviceState;
|
||||||
use tokio::task::spawn;
|
use tokio::task::spawn;
|
||||||
|
|
||||||
|
use crate::hass::DiscoveryConfig;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DeviceStates {
|
pub struct DeviceStates {
|
||||||
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
pub mi_temp_names: BTreeMap<BDAddr, String>,
|
||||||
|
|
@ -32,6 +34,11 @@ pub struct DeviceStates {
|
||||||
active_rf_temp_id: RfDeviceId,
|
active_rf_temp_id: RfDeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum HassUpdate {
|
||||||
|
Discovery(String, DiscoveryConfig),
|
||||||
|
Update(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FormattableDevice {
|
pub trait FormattableDevice {
|
||||||
fn format_open_metrics(&self) -> impl Display + '_;
|
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
|
let device = self
|
||||||
.devices
|
.devices
|
||||||
.entry(device)
|
.entry(device)
|
||||||
|
|
@ -97,11 +104,16 @@ impl DeviceStates {
|
||||||
match BDAddr::from_mi_temp_mac_part(addr) {
|
match BDAddr::from_mi_temp_mac_part(addr) {
|
||||||
Ok(addr) => {
|
Ok(addr) => {
|
||||||
if let Some(name) = self.mi_temp_names.get(&addr) {
|
if let Some(name) = self.mi_temp_names.get(&addr) {
|
||||||
let state = self
|
let state = self.mi_temp_devices.entry(addr).or_insert_with(|| {
|
||||||
.mi_temp_devices
|
let device = MiTempDevice::new(addr, name.clone());
|
||||||
.entry(addr)
|
hass_cb(HassUpdate::Discovery(
|
||||||
.or_insert_with(|| MiTempDevice::new(addr, name.clone()));
|
device.discovery_topic(),
|
||||||
|
device.mqtt_device(),
|
||||||
|
));
|
||||||
|
device
|
||||||
|
});
|
||||||
state.update(value);
|
state.update(value);
|
||||||
|
hass_cb(HassUpdate::Update(state.state_topic(), state.mqtt_state()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("Failed to parse mitemp mac: {:#}", e),
|
Err(e) => eprintln!("Failed to parse mitemp mac: {:#}", e),
|
||||||
|
|
@ -261,6 +273,28 @@ impl BDAddr {
|
||||||
address.reverse();
|
address.reverse();
|
||||||
Ok(BDAddr { address })
|
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 {
|
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 config;
|
||||||
mod device;
|
mod device;
|
||||||
|
mod hass;
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
mod topic;
|
mod topic;
|
||||||
|
|
||||||
use crate::config::{Config, ListenConfig};
|
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::mqtt::mqtt_stream;
|
||||||
use crate::topic::Topic;
|
use crate::topic::Topic;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
@ -125,11 +126,11 @@ async fn mqtt_client<S: Stream<Item = Result<Publish>>>(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
while let Some(message) = stream.next().await {
|
while let Some(message) = stream.next().await {
|
||||||
let message = message?;
|
let message = message?;
|
||||||
println!(
|
// println!(
|
||||||
"{} {}",
|
// "{} {}",
|
||||||
message.topic,
|
// message.topic,
|
||||||
std::str::from_utf8(message.payload.as_ref()).unwrap_or_default()
|
// std::str::from_utf8(message.payload.as_ref()).unwrap_or_default()
|
||||||
);
|
// );
|
||||||
let topic = Topic::from(message.topic.as_str());
|
let topic = Topic::from(message.topic.as_str());
|
||||||
|
|
||||||
match topic {
|
match topic {
|
||||||
|
|
@ -150,10 +151,33 @@ async fn mqtt_client<S: Stream<Item = Result<Publish>>>(
|
||||||
}
|
}
|
||||||
Topic::Power(_) => {}
|
Topic::Power(_) => {}
|
||||||
Topic::Result(device) | Topic::Sensor(device) | Topic::Status(device) => {
|
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();
|
let payload = std::str::from_utf8(message.payload.as_ref()).unwrap_or_default();
|
||||||
if let Ok(json) = jzon::parse(payload) {
|
if let Ok(json) = jzon::parse(payload) {
|
||||||
let mut device_states = device_states.lock().unwrap();
|
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) => {
|
Topic::Msg(_device) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue