ubmsc 0.1.1

A client for Battery Management Systems (BMS).
Documentation
use crate::{log, Client, Metrics, Result};
use prometheus::{Encoder, ProtobufEncoder, Registry, TextEncoder};
use std::io::Write;

#[derive(Clone, Copy, Default, Debug)]
pub enum Encoding {
    #[default]
    Text,
    Protobuf,
}

impl Encoding {
    pub fn from_accept(accept: impl AsRef<[u8]>) -> Option<Self> {
        let accept = accept.as_ref();
        if find_seq(accept, b"application/vnd.google.protobuf").is_some() {
            Some(Self::Protobuf)
        } else if find_seq(accept, b"text/plain").is_some() {
            Some(Self::Text)
        } else {
            None
        }
    }
}

pub struct Exporter {
    registry: Registry,
    text_encoder: TextEncoder,
    protobuf_encoder: ProtobufEncoder,
    metrics: Vec<Metrics>,
    default_encoding: Encoding,
}

impl<'x> Exporter {
    pub fn new(default_encoding: Encoding, clients: &[Client]) -> Result<Self> {
        let registry = Registry::new();
        let metrics = Vec::default();
        let text_encoder = TextEncoder::new();
        let protobuf_encoder = ProtobufEncoder::new();

        let mut this = Self {
            registry,
            text_encoder,
            protobuf_encoder,
            metrics,
            default_encoding,
        };

        this.metrics(clients)?;

        Ok(this)
    }

    fn metrics(&mut self, clients: &[Client]) -> Result<()> {
        for client in clients {
            let metric = Metrics::new(client.device_id())?;
            metric.register(Some(&self.registry))?;
            self.metrics.push(metric);
        }
        Ok(())
    }

    pub async fn scrape(&self, clients: &[Client]) -> Result<()> {
        for (client, metrics) in clients.iter().zip(self.metrics.iter()) {
            let device_id = client.device_id();
            log::info!("Scrape metrics from: '{device_id}'");

            if let Err(error) = client.open().await {
                log::error!("Error while connecting: {error}");
            } else {
                match client.device_info().await {
                    Ok(device_info) => metrics.scrape(&device_info),
                    Err(error) => {
                        log::error!("Error while fetch device info from '{device_id}': {error}")
                    }
                }
                match client.cell_data().await {
                    Ok(cell_data) => metrics.scrape(&cell_data),
                    Err(error) => {
                        log::error!("Error while fetch cell data from '{device_id}': {error}")
                    }
                }
                if let Err(error) = client.close().await {
                    log::error!("Error while disconnecting: {error}");
                }
            }
        }
        Ok(())
    }

    pub fn encode(&self, encoding: Option<Encoding>, mut output: impl Write) -> Result<&str> {
        Ok(match encoding.unwrap_or(self.default_encoding) {
            Encoding::Protobuf => {
                self.protobuf_encoder
                    .encode(&self.registry.gather(), &mut output)?;
                self.protobuf_encoder.format_type()
            }
            Encoding::Text => {
                self.text_encoder
                    .encode(&self.registry.gather(), &mut output)?;
                self.text_encoder.format_type()
            }
        })
    }
}

fn find_seq<T>(seq: &[T], sub: &[T]) -> Option<usize>
where
    for<'a> &'a [T]: PartialEq,
{
    seq.windows(sub.len()).position(|win| win == sub)
}