From 84231b5ef7315c842ff9afa97965fdc217b1341f Mon Sep 17 00:00:00 2001 From: Tijmen van Nesselrooij Date: Mon, 8 Sep 2025 18:04:27 +0200 Subject: [PATCH] Add months endpoint to electricity API --- src/electricity_api/src/database.rs | 134 ++++++++++++++++++++++++++++ src/electricity_api/src/main.rs | 107 +++++++++++++++++++++- 2 files changed, 240 insertions(+), 1 deletion(-) diff --git a/src/electricity_api/src/database.rs b/src/electricity_api/src/database.rs index 66bedbf..51205e3 100644 --- a/src/electricity_api/src/database.rs +++ b/src/electricity_api/src/database.rs @@ -1,3 +1,5 @@ +use shared_api_lib::year_month::YearMonth; + pub fn get_day_power_entities(date: &chrono::NaiveDate, database_path: &str) -> Vec { let mut items = Vec::new(); @@ -195,3 +197,135 @@ pub struct LogDateSummary { pub total_power_return_night: f64, pub gas_consumption_in_cubic_meters: f64, } + +pub fn get_month_power_summaries( + start: &YearMonth, + stop: &YearMonth, + database_path: &str, +) -> Vec { + let mut items = Vec::new(); + + let maybe_connection = rusqlite::Connection::open(database_path); + if maybe_connection.is_err() { + log::error!( + "Failed to open database connection to {} with error {}", + database_path, + maybe_connection.unwrap_err() + ); + return items; + } + + let connection = maybe_connection.unwrap(); + let maybe_statement = connection.prepare( + "SELECT + STRFTIME('%Y-%m', \"Date\") AS YearMonth, + MAX(TotalPowerConsumptionDay), + MIN(TotalPowerConsumptionDay), + MAX(TotalPowerConsumptionNight), + MIN(TotalPowerConsumptionNight), + MAX(TotalPowerReturnDay), + MIN(TotalPowerReturnDay), + MAX(TotalPowerReturnNight), + MIN(TotalPowerReturnNight), + MAX(GasConsumptionInCubicMeters), + MIN(GasConsumptionInCubicMeters) + FROM \"ElectricityLog\" + WHERE \"Date\" >= :start_date AND \"Date\" <= :stop_date + GROUP BY YearMonth + ORDER BY YearMonth;", + ); + if maybe_statement.is_err() { + log::error!( + "Failed to prepate database statement with error {}", + maybe_statement.unwrap_err() + ); + return items; + } + + let formatted_start = format!("{}-01", start); + let formatted_stop = format!("{}-31", stop); + log::info!( + "Fetching month power data for date range {} to {}", + formatted_start, + formatted_stop + ); + let mut statement = maybe_statement.unwrap(); + let maybe_row_iterator = statement.query_map( + &[ + (":start_date", &formatted_start), + (":stop_date", &formatted_stop), + ], + |row| { + Ok(LogMonthSummaryEntity { + year_month: row.get(0)?, + total_power_consumption_day: row.get::(1)? + - row.get::(2)?, + total_power_consumption_night: row.get::(3)? + - row.get::(4)?, + total_power_return_day: row.get::(5)? - row.get::(6)?, + total_power_return_night: row.get::(7)? - row.get::(8)?, + gas_consumption_in_cubic_meters: row.get::(9)? + - row.get::(10)?, + }) + }, + ); + + match maybe_row_iterator { + Ok(iterator) => { + for row in iterator { + match row { + Ok(entity) => { + if let Some(year_month) = YearMonth::try_parse(&entity.year_month) { + items.push(LogMonthSummary { + year: year_month.year, + month: year_month.month, + total_power_consumption_day: entity.total_power_consumption_day, + total_power_consumption_night: entity.total_power_consumption_night, + total_power_return_day: entity.total_power_return_day, + total_power_return_night: entity.total_power_return_night, + gas_consumption_in_cubic_meters: entity + .gas_consumption_in_cubic_meters, + }); + } else { + log::error!( + "Failed to parse year month {} from SQL row", + entity.year_month + ); + } + } + Err(error) => log::error!( + "Failed to interpret row from SQL query with error {}", + error + ), + } + } + } + Err(error) => { + log::error!( + "Failed to execute month power data SQL query with error {}", + error + ); + } + } + + items +} + +struct LogMonthSummaryEntity { + pub year_month: String, + pub total_power_consumption_day: f64, + pub total_power_consumption_night: f64, + pub total_power_return_day: f64, + pub total_power_return_night: f64, + pub gas_consumption_in_cubic_meters: f64, +} + +pub struct LogMonthSummary { + pub year: i32, + pub month: u8, + pub total_power_consumption_day: f64, + pub total_power_consumption_night: f64, + pub total_power_return_day: f64, + pub total_power_return_night: f64, + pub gas_consumption_in_cubic_meters: f64, +} diff --git a/src/electricity_api/src/main.rs b/src/electricity_api/src/main.rs index 1b068ca..50270a8 100644 --- a/src/electricity_api/src/main.rs +++ b/src/electricity_api/src/main.rs @@ -131,7 +131,61 @@ async fn serve(configuration: &std::sync::Arc) { }, ); - warp::serve(day.or(days)) + let months = warp::get() + .and(warp::path("months")) + .and(warp::query::>()) + .and(warp::header::headers_cloned()) + .and(warp::any().map({ + let configuration = configuration.clone(); + move || configuration.clone() + })) + .map( + |query: HashMap, + headers, + configuration: std::sync::Arc| { + if !has_required_header( + &headers, + &configuration.http_header_name_to_validate, + &configuration.http_header_value_to_validate, + ) { + log::info!("Access requested to /months with invalid header value"); + return Response::builder() + .status(403) + .body(String::from("Forbidden")); + } + + let maybe_start = + shared_api_lib::query_helpers::try_parse_query_month_year(query.get("start")); + if maybe_start.is_none() { + return Response::builder() + .status(400) + .body(String::from("Unsupported \"start\" param in query.")); + } + + let maybe_stop = + shared_api_lib::query_helpers::try_parse_query_month_year(query.get("stop")); + if maybe_stop.is_none() { + return Response::builder() + .status(400) + .body(String::from("Unsupported \"stop\" param in query.")); + } + + let start = maybe_start.unwrap(); + let stop = maybe_stop.unwrap(); + if start > stop { + return Response::builder().status(400).body(String::from( + "Param \"start\" must be smaller than or equal to param \"stop\" in query.", + )); + } + + let json = get_months_power_json(&start, &stop, &configuration.database_path); + return Response::builder() + .header("Content-Type", "application/json") + .body(json); + }, + ); + + warp::serve(day.or(days).or(months)) .run(([127, 0, 0, 1], configuration.listening_port)) .await; } @@ -301,3 +355,54 @@ struct DaysResponseItem { total_power_return: f64, total_gas_use: f64, } + +fn get_months_power_json( + start: &shared_api_lib::year_month::YearMonth, + stop: &shared_api_lib::year_month::YearMonth, + database_path: &str, +) -> String { + let summaries = database::get_month_power_summaries(&start, &stop, &database_path); + let mut response_model = MonthsResponse { + month_logs: Vec::new(), + }; + + for summary in summaries { + response_model.month_logs.push(MonthsResponseItem { + year: summary.year, + month: summary.month, + total_power_use: summary.total_power_consumption_day + + summary.total_power_consumption_night, + total_power_return: summary.total_power_return_day + summary.total_power_return_night, + total_gas_use: summary.gas_consumption_in_cubic_meters, + }); + } + + match serde_json::to_string(&response_model) { + Ok(json) => json, + Err(error) => { + log::error!( + "Failed to format JSON data for date range {} to {} with error {}", + start, + stop, + error + ); + return String::from("[]"); + } + } +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct MonthsResponse { + month_logs: Vec, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct MonthsResponseItem { + year: i32, + month: u8, + total_power_use: f64, + total_power_return: f64, + total_gas_use: f64, +}