allow using config file instead of env

This commit is contained in:
Robin Appelman 2024-10-27 13:51:54 +01:00
commit 7f7ad384a8
7 changed files with 357 additions and 33 deletions

View file

@ -1,23 +1,86 @@
use crate::device::{BDAddr, RfDeviceId};
use color_eyre::{eyre::WrapErr, Report, Result};
use rumqttc::MqttOptions;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::fs::read_to_string;
use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
#[derive(Default)]
#[derive(Debug, Deserialize)]
pub struct Config {
pub mqtt_host: String,
pub mqtt_port: u16,
pub host_port: u16,
pub mi_temp_names: BTreeMap<BDAddr, String>,
pub rf_temp_names: HashMap<RfDeviceId<'static>, String>,
pub mqtt_credentials: Option<Credentials>,
pub listen: ListenConfig,
pub names: NamesConfig,
pub mqtt: MqttConfig,
}
pub struct Credentials {
username: String,
password: String,
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum ListenConfig {
Ip {
#[serde(default = "default_address")]
address: IpAddr,
port: u16,
},
Unix {
path: String,
},
}
fn default_address() -> IpAddr {
Ipv4Addr::UNSPECIFIED.into()
}
#[derive(Debug, Deserialize)]
pub struct NamesConfig {
#[serde(rename = "mitemp")]
pub mi_temp: BTreeMap<BDAddr, String>,
#[serde(rename = "rftemp")]
pub rf_temp: HashMap<RfDeviceId<'static>, String>,
}
#[derive(Debug, Deserialize)]
pub struct MqttConfig {
#[serde(rename = "hostname")]
host: String,
#[serde(default = "default_mqtt_port")]
port: u16,
#[serde(flatten)]
credentials: Option<Credentials>,
}
fn default_mqtt_port() -> u16 {
1883
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Credentials {
Raw {
username: String,
password: String,
},
File {
username: String,
password_file: String,
},
}
impl Credentials {
pub fn username(&self) -> String {
match self {
Credentials::Raw { username, .. } => username.clone(),
Credentials::File { username, .. } => username.clone(),
}
}
pub fn password(&self) -> String {
match self {
Credentials::Raw { password, .. } => password.clone(),
Credentials::File { password_file, .. } => secretfile::load(password_file).unwrap(),
}
}
}
impl Config {
@ -68,32 +131,44 @@ impl Config {
Ok(username) => {
let password = dotenvy::var("MQTT_PASSWORD")
.wrap_err("MQTT_USERNAME set, but MQTT_PASSWORD not set")?;
Some(Credentials { username, password })
Some(Credentials::Raw { username, password })
}
Err(_) => None,
};
Ok(Config {
mqtt_host,
mqtt_port,
host_port,
mi_temp_names,
rf_temp_names,
mqtt_credentials,
listen: ListenConfig::Ip {
port: host_port,
address: default_address(),
},
names: NamesConfig {
mi_temp: mi_temp_names,
rf_temp: rf_temp_names,
},
mqtt: MqttConfig {
port: mqtt_port,
host: mqtt_host,
credentials: mqtt_credentials,
},
})
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> {
let raw = read_to_string(path)?;
Ok(toml::from_str(&raw)?)
}
pub fn mqtt(&self) -> Result<MqttOptions> {
let hostname = hostname::get()?
.into_string()
.map_err(|_| Report::msg("invalid hostname"))?;
let mut mqtt_options = MqttOptions::new(
format!("taspromto-{}", hostname),
&self.mqtt_host,
self.mqtt_port,
&self.mqtt.host,
self.mqtt.port,
);
if let Some(credentials) = self.mqtt_credentials.as_ref() {
mqtt_options.set_credentials(&credentials.username, &credentials.password);
if let Some(credentials) = self.mqtt.credentials.as_ref() {
mqtt_options.set_credentials(credentials.username(), credentials.password());
}
mqtt_options.set_keep_alive(Duration::from_secs(5));
Ok(mqtt_options)

View file

@ -1,6 +1,8 @@
use color_eyre::{eyre::WrapErr, Report, Result};
use jzon::JsonValue;
use rumqttc::{AsyncClient, QoS};
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
@ -565,6 +567,16 @@ pub struct BDAddr {
pub address: [u8; 6usize],
}
impl<'de> Deserialize<'de> for BDAddr {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = <Cow<'de, str>>::deserialize(deserializer)?;
Self::from_mi_temp_mac_part(&str).map_err(D::Error::custom)
}
}
impl BDAddr {
/// parse BDAddr from the last 6 characters of the mac address
/// first 6 characters are always set to 582D34
@ -806,6 +818,16 @@ impl RfDeviceId<'_> {
}
}
impl<'de> Deserialize<'de> for RfDeviceId<'static> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = <Cow<'de, str>>::deserialize(deserializer)?;
Self::from_str(&str).map_err(D::Error::custom)
}
}
impl FromStr for RfDeviceId<'static> {
type Err = ParseIntError;

View file

@ -3,13 +3,14 @@ mod device;
mod mqtt;
mod topic;
use crate::config::Config;
use crate::config::{Config, ListenConfig};
use crate::device::{
format_device_state, format_dsmr_state, format_mi_temp_state, format_rf_temp_state, Device,
DeviceStates,
};
use crate::mqtt::mqtt_stream;
use crate::topic::Topic;
use clap::Parser;
use color_eyre::{eyre::WrapErr, Result};
use pin_utils::pin_mut;
@ -18,14 +19,28 @@ use rumqttc::{AsyncClient, Publish, QoS};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::net::UnixListener;
use tokio::task::spawn;
use tokio::time::{sleep, Duration};
use tokio_stream::wrappers::UnixListenerStream;
use tokio_stream::{Stream, StreamExt};
use warp::Filter;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Config file to use, if omitted the config will be loaded from environment variables
config: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::from_env()?;
let args = Args::parse();
let config = match args.config {
Some(path) => Config::from_file(path)?,
_ => Config::from_env()?,
};
let mqtt_options = config.mqtt()?;
let device_states = <Arc<Mutex<DeviceStates>>>::default();
@ -57,9 +72,8 @@ async fn main() -> Result<()> {
}
async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
let host_port = config.host_port;
let mi_temp_names = config.mi_temp_names.clone();
let rf_temp_names = config.rf_temp_names.clone();
let mi_temp_names = config.names.mi_temp.clone();
let rf_temp_names = config.names.rf_temp.clone();
let state = warp::any().map(move || device_states.clone());
@ -83,7 +97,16 @@ async fn serve(device_states: Arc<Mutex<DeviceStates>>, config: Config) {
response
});
warp::serve(metrics).run(([0, 0, 0, 0], host_port)).await;
match config.listen {
ListenConfig::Ip { address, port } => {
warp::serve(metrics).run((address, port)).await;
}
ListenConfig::Unix { path } => {
let listener = UnixListener::bind(path).unwrap();
let incoming = UnixListenerStream::new(listener);
warp::serve(metrics).run_incoming(incoming).await;
}
}
}
async fn command(client: &AsyncClient, device: &Device, command: &str, body: &str) -> Result<()> {