Skip to main content

tanuki_bthome/
lib.rs

1use std::collections::{HashMap, hash_map::Entry};
2
3use heck::ToSnakeCase as _;
4use tanuki::{
5    TanukiConnection,
6    capabilities::{Authority, sensor::Sensor},
7};
8use tanuki_common::{capabilities::sensor::SensorPayload, meta};
9
10mod bthome;
11
12type Result<T> = std::result::Result<T, Error>;
13
14#[derive(thiserror::Error, Debug)]
15pub enum Error {
16    #[error("btleplug error: {0}")]
17    Btleplug(#[from] btleplug::Error),
18    #[error("tanuki error: {0}")]
19    Tanuki(#[from] tanuki::Error),
20}
21
22pub async fn bridge(
23    addr: &str,
24    id_map: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>, impl AsRef<str>)>,
25) -> Result<()> {
26    let id_map = id_map
27        .into_iter()
28        .map(|(k, id, name)| {
29            (k.as_ref().to_owned(), (id.as_ref().to_owned(), name.as_ref().to_owned()))
30        })
31        .collect::<HashMap<_, _>>();
32
33    let tanuki = TanukiConnection::connect("tanuki-bthome", addr).await?;
34
35    tokio::spawn({
36        let tanuki = tanuki.clone();
37
38        async move {
39            loop {
40                let packet = tanuki.recv_raw().await;
41                tracing::debug!("Received packet: {packet:?}");
42            }
43        }
44    });
45
46    let mut updates = bthome::event_stream().await?;
47
48    let mut devices = HashMap::<String, Sensor<Authority>>::new();
49
50    loop {
51        let update = updates
52            .recv()
53            .await
54            .expect("bluetooth event stream ended")?;
55
56        tracing::debug!("BTHome update: {update:#?}");
57
58        let entry = devices.entry(update.address.clone());
59        let sensor = match entry {
60            Entry::Occupied(entry) => entry,
61            Entry::Vacant(entry) => {
62                tracing::info!(?update.name, ?update.address, "Registering new device");
63
64                let (id, name) = id_map
65                    .get(update.name.as_str())
66                    .or_else(|| id_map.get(update.address.as_str()))
67                    .cloned()
68                    .unwrap_or((update.name.to_snake_case(), update.name));
69
70                let entity = tanuki.entity(id).await?;
71
72                entity.publish_meta(meta::Name(name.into())).await?;
73                entity
74                    .publish_meta(meta::Type("BTHome Sensor".into()))
75                    .await?;
76                entity
77                    .publish_meta(meta::Provider("tanuki-bthome".into()))
78                    .await?;
79
80                let sensor = entity.capability::<Sensor<_>>().await?;
81                entry.insert_entry(sensor)
82            }
83        };
84
85        for object in &update.objects {
86            sensor
87                .get()
88                .publish(object.topic(), SensorPayload {
89                    value: object.value(),
90                    unit: object.unit().into(),
91                    timestamp: update.timestamp,
92                })
93                .await?;
94        }
95    }
96}