This is some shit

This commit is contained in:
Mikko Ahlroth 2024-07-03 22:32:29 +03:00
parent 1a64f23eae
commit 05b7a81139
5 changed files with 313 additions and 10 deletions

View file

@ -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]

View file

@ -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" }

View file

@ -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, []),
])
}

View file

@ -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)
}