use crate::config::Config;
use brewfatherlog::brewfather::{Brewfather, BrewfatherLoggingEvent};
use brewfatherlog::grainfather::{Fermenter, FermenterId, Grainfather, TemperatureRecord};
use log::{debug, error, info, warn};
use simplelog::{
ColorChoice, CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger,
format_description,
};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::time::sleep;
mod config;
pub const PROGRAM_NAME: &str = env!("CARGO_PKG_NAME");
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
fn program_dir_path() -> PathBuf {
let home_dir: PathBuf = dirs::home_dir().expect("Unable to get home directory");
home_dir.join(format!(".{PROGRAM_NAME}"))
}
fn config_file_path() -> PathBuf {
program_dir_path().join(format!("{PROGRAM_NAME}.toml"))
}
fn log_file_path() -> PathBuf {
program_dir_path().join(format!("{PROGRAM_NAME}.log"))
}
fn init_logging() {
let config = simplelog::ConfigBuilder::new()
.set_time_format_custom(format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
))
.set_time_offset_to_local()
.expect("failed to set time offset to local")
.build();
let log_file_path = log_file_path();
std::fs::create_dir_all(log_file_path.parent().unwrap())
.expect("failed to create the program directory");
let log_file = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(log_file_path)
.expect("failed to open log file");
CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Info, config.clone(), TerminalMode::Mixed, ColorChoice::Auto),
WriteLogger::new(LevelFilter::Info, config, log_file),
])
.expect("failed to initialize loggers");
}
async fn log_temperature(
brewfather: &Brewfather,
last_logged: &mut HashMap<FermenterId, OffsetDateTime>,
fermenter: &Fermenter,
temp_record: TemperatureRecord,
) {
let now = OffsetDateTime::now_utc();
let age: time::Duration = now - temp_record.timestamp;
if age > Duration::from_secs(30 * 60) {
let age = Duration::from_secs(age.whole_seconds().try_into().expect("fail to convert age"));
warn!(
"Ignoring temperature {:.02} °C of fermenter \"{}\" because the temperature is too old ({}).",
temp_record.temperature,
fermenter.name,
humantime::format_duration(age),
);
return;
}
if last_logged.get(&fermenter.id).is_some_and(|last| now <= *last) {
warn!(
"Ignoring temperature {:.02} °C of fermenter \"{}\" because we already logged it.",
temp_record.temperature, fermenter.name,
);
return;
}
let event = BrewfatherLoggingEvent { name: &fermenter.name, temp: temp_record.temperature };
match brewfather.log(event).await {
Ok(()) => {
info!("Logged temperature of fermenter \"{}\" to brewfather.", fermenter.name);
last_logged.insert(fermenter.id, temp_record.timestamp);
}
Err(err) => {
error!(
"Error logging the temperature of fermenter \"{}\" to brewfather: {}",
fermenter.name, err
);
}
}
}
async fn main_loop(config: Config) -> ! {
let init_grainfather =
|| Grainfather::new(&config.grainfather.auth.email, &config.grainfather.auth.password);
info!("Starting {PROGRAM_NAME} v{VERSION}.");
let mut last_logged: HashMap<FermenterId, OffsetDateTime> = HashMap::new();
let brewfather = Brewfather::new(config.brewfather.logging_id)
.expect("error initializing brewfather client");
loop {
let grainfather = match init_grainfather().await {
Ok(grainfather) => grainfather,
Err(err) => {
error!("Error initializing grainfather client: {err}");
sleep(Duration::from_secs(10)).await;
continue;
}
};
let ferms = match grainfather.list_fermenters().await {
Ok(ferms) => ferms,
Err(err) => {
error!("Error getting fermenters: {err}");
sleep(Duration::from_secs(10)).await;
continue;
}
};
if ferms.is_empty() {
info!("No fermenters found.");
}
for ferm in ferms {
match grainfather.get_fermenter_temperature(ferm.id).await {
Ok(Some(temp_record)) => {
info!("Fermenter \"{}\": {:.02} °C", ferm.name, temp_record.temperature);
log_temperature(&brewfather, &mut last_logged, &ferm, temp_record).await;
}
Ok(None) => {
debug!("No recent temperature record of fermenter \"{}\".", ferm.name);
}
Err(err) => {
error!("Error getting temperature of fermenter \"{}\": {}", ferm.name, err);
}
}
}
sleep(Duration::from_secs(15 * 60 + 1)).await;
}
}
#[tokio::main]
async fn main() {
let config_file = config_file_path();
let created_config_file = Config::create_file_if_nonexistent(&config_file);
if created_config_file {
println!(
"Created configuration file on \"{}\". Please edit it and run {} again.",
config_file.display(),
PROGRAM_NAME,
);
std::process::exit(0);
}
let config: Config = Config::from_config_file(&config_file);
init_logging();
main_loop(config).await;
}