From 05b7a81139f78a6c8b35ed210b9fd24cdc43c817 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Wed, 3 Jul 2024 22:32:29 +0300 Subject: [PATCH] This is some shit --- backend/gleam.toml | 1 + backend/manifest.toml | 7 +- backend/src/ap_systems/api.gleam | 50 +++- backend/src/aurinko/updater.gleam | 264 +++++++++++++++++++ backend/src/aurinko/web/templates/base.gleam | 1 - 5 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 backend/src/aurinko/updater.gleam delete mode 100644 backend/src/aurinko/web/templates/base.gleam diff --git a/backend/gleam.toml b/backend/gleam.toml index 493282e..113887b 100644 --- a/backend/gleam.toml +++ b/backend/gleam.toml @@ -30,5 +30,6 @@ gleam_json = ">= 1.0.1 and < 2.0.0" envoy = ">= 1.0.1 and < 2.0.0" mist = ">= 1.2.0 and < 2.0.0" aurinko_common = { path = "../common" } +gleamy_structures = ">= 1.0.0 and < 2.0.0" [dev-dependencies] diff --git a/backend/manifest.toml b/backend/manifest.toml index f58b188..a4ef84a 100644 --- a/backend/manifest.toml +++ b/backend/manifest.toml @@ -2,6 +2,7 @@ # You typically do not need to edit this file packages = [ + { name = "aurinko_common", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../common" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, { name = "biscotto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "biscotto", source = "hex", outer_checksum = "A1CFEA1686FA8ABDE90B76E22775FF29EE8156A64DAC327F48141A68951D662C" }, { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, @@ -9,7 +10,6 @@ packages = [ { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "form_coder", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "form_coder", source = "hex", outer_checksum = "FA27C97AADF66A79E4EEE78D30A50C18660A9FAFD91C27A5BC780DBC753CB8AF" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, @@ -17,6 +17,7 @@ packages = [ { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleamy_structures", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleamy_structures", source = "hex", outer_checksum = "FF1B7600123B2B7C15E91BEBE6F417B4003928BF4282EF01A74A792B4ED6985A" }, { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, @@ -24,7 +25,6 @@ packages = [ { name = "htmgrrrl", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "htmerl"], otp_app = "htmgrrrl", source = "hex", outer_checksum = "983492567967DAA64776E005B9E70353368B14E6F87D543E01308B48A7A0398F" }, { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" }, { name = "lustre", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "43642C0602D3E2D6FEC3E24173D68A1F8E646969B53A2B0A5EB61238DDA739C4" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, @@ -35,6 +35,7 @@ packages = [ ] [requirements] +aurinko_common = { path = "../common" } birl = { version = ">= 1.7.1 and < 2.0.0" } biscotto = { version = ">= 1.0.0 and < 2.0.0" } envoy = { version = ">= 1.0.1 and < 2.0.0" } @@ -45,9 +46,9 @@ gleam_httpc = { version = ">= 2.2.0 and < 3.0.0" } gleam_json = { version = ">= 1.0.1 and < 2.0.0" } gleam_otp = { version = ">= 0.10.0 and < 1.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleamy_structures = { version = ">= 1.0.0 and < 2.0.0"} htmgrrrl = { version = ">= 0.3.0 and < 1.0.0" } lustre = { version = ">= 4.3.0 and < 5.0.0" } -lustre_ui = { version = ">= 0.6.0 and < 1.0.0"} mist = { version = ">= 1.2.0 and < 2.0.0" } sqlight = { version = ">= 0.9.0 and < 1.0.0" } wisp = { version = ">= 0.15.0 and < 1.0.0" } diff --git a/backend/src/ap_systems/api.gleam b/backend/src/ap_systems/api.gleam index 1da5e9f..6f06114 100644 --- a/backend/src/ap_systems/api.gleam +++ b/backend/src/ap_systems/api.gleam @@ -2,6 +2,7 @@ import birl import biscotto import form_coder import gleam/dynamic +import gleam/float import gleam/http import gleam/http/request import gleam/httpc @@ -21,6 +22,18 @@ pub type APIError { ScrapeError } +pub type ProductionInfo { + ProductionInfo( + co2: Float, + duration: Int, + ecu_sign: String, + last_power: Float, + lifetime: Float, + meter_flag: Int, + today: Float, + ) +} + pub type ModuleIDs { ModuleIDs(sid: String, vids: List(String)) } @@ -75,12 +88,29 @@ pub fn login(username: String, password: String) { pub fn get_production(cookies: biscotto.CookieJar) { let assert Ok(req) = request.from_uri(production_info_url()) - req - |> request.set_method(http.Post) - |> set_common_headers() - |> biscotto.with_cookies(cookies) - |> httpc.send() - |> result.map_error(RequestError) + use resp <- result.try( + req + |> request.set_method(http.Post) + |> set_common_headers() + |> biscotto.with_cookies(cookies) + |> httpc.send() + |> result.map_error(RequestError), + ) + + json.decode( + resp.body, + dynamic.decode7( + ProductionInfo, + dynamic.field("co2", float_string_decoder), + dynamic.field("duration", dynamic.int), + dynamic.field("ecu_sign", dynamic.string), + dynamic.field("last_power", float_string_decoder), + dynamic.field("lifetime", float_string_decoder), + dynamic.field("meter_flag", dynamic.int), + dynamic.field("today", float_string_decoder), + ), + ) + |> result.map_error(DecodeError) } pub fn get_module_ids(cookies: biscotto.CookieJar) { @@ -238,3 +268,11 @@ fn module_power_url() { path: root_path <> ajax_view_root <> "/getViewPowerByViewAjax", ) } + +fn float_string_decoder(str: dynamic.Dynamic) { + use value <- result.try(dynamic.string(str)) + float.parse(value) + |> result.replace_error([ + dynamic.DecodeError("a string representing a float", value, []), + ]) +} diff --git a/backend/src/aurinko/updater.gleam b/backend/src/aurinko/updater.gleam new file mode 100644 index 0000000..9575e90 --- /dev/null +++ b/backend/src/aurinko/updater.gleam @@ -0,0 +1,264 @@ +import ap_systems/api +import ap_systems/module_power +import birl +import birl/duration +import biscotto +import gleam/dict +import gleam/dynamic +import gleam/list +import gleam/option +import gleam/order +import gleam/otp/actor +import gleam/result +import gleamy/red_black_tree_set.{type Set} + +const login_cookie_expiry = 86_400 + +pub type Message { + Update +} + +pub type ProductionInfo = + api.ProductionInfo + +pub type ModuleIDs = + api.ModuleIDs + +pub type MomentaryPower = + module_power.MomentaryPower + +pub type ModulePower { + ModulePower(id: String, power: dict.Dict(birl.Time, MomentaryPower)) +} + +pub type ModuleDataset { + ModuleDataset(times: Set(birl.Time), datas: dict.Dict(String, ModulePower)) +} + +pub type Dataset { + Dataset(production_info: ProductionInfo, modules: ModuleDataset) +} + +pub type State { + InitialState( + username: String, + password: String, + error: option.Option(UpdateError), + ) + LoadedState( + username: String, + password: String, + cookies: biscotto.CookieJar, + cookies_acquired: birl.Time, + ids: ModuleIDs, + dataset: option.Option(Dataset), + error: option.Option(UpdateError), + ) +} + +pub type UpdateError { + APIError(err: api.APIError) + DecodeError(err: dynamic.DecodeError) +} + +pub fn start(username: String, password: String) { + actor.start(InitialState(username, password, option.None), handle_message) +} + +fn handle_message(message: Message, state: State) -> actor.Next(Message, State) { + let next = + with_loaded_state( + state, + fn(username, password, cookies, cookies_acquired, ids, dataset) { + case message { + Update -> { + use #(cookies, cookies_acquired, new_dataset) <- result.try(update( + username, + password, + cookies, + cookies_acquired, + ids, + dataset, + )) + + let new_state = + LoadedState( + username, + password, + cookies, + cookies_acquired, + ids, + option.Some(new_dataset), + option.None, + ) + + Ok(actor.continue(new_state)) + } + } + }, + ) + + case next { + Ok(next) -> next + Error(err) -> { + let new_state = case state { + InitialState(username, password, ..) -> + InitialState(username, password, error: option.Some(err)) + LoadedState( + username, + password, + cookies, + cookies_acquired, + ids, + dataset, + .., + ) -> + LoadedState( + username, + password, + cookies, + cookies_acquired, + ids, + dataset, + option.Some(err), + ) + } + actor.continue(new_state) + } + } +} + +fn with_loaded_state( + state: State, + fun: fn( + String, + String, + biscotto.CookieJar, + birl.Time, + ModuleIDs, + option.Option(Dataset), + ) -> + Result(actor.Next(Message, State), UpdateError), +) { + case state { + InitialState(username, password, ..) -> { + use loaded_state <- result.try( + init(username, password) |> result.map_error(APIError), + ) + fun( + username, + password, + loaded_state.0, + loaded_state.1, + loaded_state.2, + loaded_state.3, + ) + } + LoadedState(username, password, cookies, cookies_acquired, ids, dataset, ..) -> + fun(username, password, cookies, cookies_acquired, ids, dataset) + } +} + +fn login(username: String, password: String) { + use cookies <- result.try(api.login(username, password)) + Ok(#(cookies, birl.utc_now())) +} + +fn init(username: String, password: String) { + use #(cookies, cookies_acquired) <- result.try(login(username, password)) + use module_ids <- result.try(api.get_module_ids(cookies)) + + Ok(#(cookies, cookies_acquired, module_ids, option.None)) +} + +fn update( + username: String, + password: String, + cookies: biscotto.CookieJar, + cookies_acquired: birl.Time, + module_ids: ModuleIDs, + dataset: option.Option(Dataset), +) { + let now = birl.utc_now() + let max_lifetime = duration.seconds(login_cookie_expiry) + let should_renew = case + duration.compare(birl.difference(now, cookies_acquired), max_lifetime) + { + order.Gt | order.Eq -> True + _ -> False + } + + let cookies_result = case should_renew { + False -> Ok(#(cookies, cookies_acquired)) + True -> login(username, password) + } + + use #(cookies, cookies_acquired) <- result.try( + cookies_result |> result.map_error(APIError), + ) + + use production_info <- result.try( + api.get_production(cookies) |> result.map_error(APIError), + ) + use module_datas <- result.try( + api.get_module_power(cookies, module_ids) |> result.map_error(APIError), + ) + use parsed_module_datas <- result.try( + module_power.decode_api_data(module_datas) + |> result.map_error(DecodeError), + ) + + let new_dataset = case dataset { + option.Some(existing_dataset) -> + Dataset(..existing_dataset, production_info: production_info) + option.None -> + Dataset( + production_info, + ModuleDataset(times: new_ordered_set(), datas: dict.new()), + ) + } + + let new_dataset = + Dataset( + ..new_dataset, + modules: add_module_datasets(new_dataset.modules, parsed_module_datas), + ) + + Ok(#(cookies, cookies_acquired, new_dataset)) +} + +fn add_module_datasets( + to: ModuleDataset, + data: module_power.CombinedDailyPower, +) -> ModuleDataset { + let to = + list.fold(data.times, to, fn(acc, time) { + ModuleDataset(..acc, times: red_black_tree_set.insert(acc.times, time)) + }) + + ModuleDataset( + ..to, + datas: dict.fold( + data.modules, + to.datas, + fn(acc, id, module: module_power.ModuleDailyPower) { + let module_power = + result.unwrap( + dict.get(acc, module.id), + ModulePower(id: id, power: dict.new()), + ) + let module_power = + ModulePower( + ..module_power, + power: dict.merge(module_power.power, module.power), + ) + + dict.insert(acc, id, module_power) + }, + ), + ) +} + +fn new_ordered_set() -> Set(birl.Time) { + red_black_tree_set.new(birl.compare) +} diff --git a/backend/src/aurinko/web/templates/base.gleam b/backend/src/aurinko/web/templates/base.gleam deleted file mode 100644 index 8b13789..0000000 --- a/backend/src/aurinko/web/templates/base.gleam +++ /dev/null @@ -1 +0,0 @@ -