allow passing trough mitemp sensors

This commit is contained in:
Robin Appelman 2020-12-14 22:02:14 +01:00
commit f7938d1d95
5 changed files with 222 additions and 22 deletions

18
Cargo.lock generated
View file

@ -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",

View file

@ -17,3 +17,4 @@ ctrlc = { version = "3.1.7", features = ["termination"] }
color-eyre = "0.5.7"
async-stream = "0.3.0"
pin-utils = "0.1.0"
hostname = "^0.3"

View file

@ -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,
})
}
}

View file

@ -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> {
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 state.state { 1 } else { 0 }
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)
}
}

View file

@ -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);