tanuki-bthome 0.0.1

BTHome adapter for Tanuki
Documentation
use std::collections::{HashMap, hash_map::Entry};

use heck::ToSnakeCase as _;
use tanuki::{
    TanukiConnection,
    capabilities::{Authority, sensor::Sensor},
};
use tanuki_common::{capabilities::sensor::SensorPayload, meta};

mod bthome;

type Result<T> = std::result::Result<T, Error>;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("btleplug error: {0}")]
    Btleplug(#[from] btleplug::Error),
    #[error("tanuki error: {0}")]
    Tanuki(#[from] tanuki::Error),
}

pub async fn bridge(
    addr: &str,
    id_map: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>, impl AsRef<str>)>,
) -> Result<()> {
    let id_map = id_map
        .into_iter()
        .map(|(k, id, name)| {
            (k.as_ref().to_owned(), (id.as_ref().to_owned(), name.as_ref().to_owned()))
        })
        .collect::<HashMap<_, _>>();

    let tanuki = TanukiConnection::connect("tanuki-bthome", addr).await?;

    tokio::spawn({
        let tanuki = tanuki.clone();

        async move {
            loop {
                let packet = tanuki.recv_raw().await;
                tracing::debug!("Received packet: {packet:?}");
            }
        }
    });

    let mut updates = bthome::event_stream().await?;

    let mut devices = HashMap::<String, Sensor<Authority>>::new();

    loop {
        let update = updates
            .recv()
            .await
            .expect("bluetooth event stream ended")?;

        tracing::debug!("BTHome update: {update:#?}");

        let entry = devices.entry(update.address.clone());
        let sensor = match entry {
            Entry::Occupied(entry) => entry,
            Entry::Vacant(entry) => {
                tracing::info!(?update.name, ?update.address, "Registering new device");

                let (id, name) = id_map
                    .get(update.name.as_str())
                    .or_else(|| id_map.get(update.address.as_str()))
                    .cloned()
                    .unwrap_or((update.name.to_snake_case(), update.name));

                let entity = tanuki.entity(id).await?;

                entity.publish_meta(meta::Name(name.into())).await?;
                entity
                    .publish_meta(meta::Type("BTHome Sensor".into()))
                    .await?;
                entity
                    .publish_meta(meta::Provider("tanuki-bthome".into()))
                    .await?;

                let sensor = entity.capability::<Sensor<_>>().await?;
                entry.insert_entry(sensor)
            }
        };

        for object in &update.objects {
            sensor
                .get()
                .publish(object.topic(), SensorPayload {
                    value: object.value(),
                    unit: object.unit().into(),
                    timestamp: update.timestamp,
                })
                .await?;
        }
    }
}