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}