Implement pump view
This commit is contained in:
parent
28c09b5086
commit
3695d4874f
16 changed files with 1470 additions and 32 deletions
|
@ -2,15 +2,15 @@
|
|||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "gleam_fetch", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "D0C9E9CAE8D6EFCCC3A9FF817DCA9ED327097222086D91DE4F6CA8FBAB02D79F" },
|
||||
{ name = "gleam_fetch", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_http"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "D0C9E9CAE8D6EFCCC3A9FF817DCA9ED327097222086D91DE4F6CA8FBAB02D79F" },
|
||||
{ name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" },
|
||||
{ name = "gleam_javascript", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "BFEBB63ABE4A1694E07DEFD19B160C2980304B5D775A89D4B02E7DE7C9D8008B" },
|
||||
{ name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" },
|
||||
{ name = "gleam_stdlib", version = "0.30.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "8D8BF3790AA31176B1E1C0B517DD74C86DA8235CF3389EA02043EE4FD82AE3DC" },
|
||||
{ name = "lustre", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9A18A7588776C14FD14D3838DF5881EF12C611C2F55637DA528A60BCCA135B41" },
|
||||
{ name = "plinth", version = "0.1.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "E40A48FAA3AB9410803AB937BE620692D86B7ABB46459A83E8C674B82CFFD05B" },
|
||||
{ name = "lustre", version = "3.0.5", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "6DD8FC3238623EF3CEC425780596C13FC7F7FFD53B3E44073669B61E6B5E4F02" },
|
||||
{ name = "plinth", version = "0.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_javascript"], otp_app = "plinth", source = "hex", outer_checksum = "E81BA6A6CEAFFADBCB85B04DC817A4CDC43AFA7BB6AE56CE0B7C7E66D1C9ADD1" },
|
||||
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
||||
{ name = "varasto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "0621E5BFD0B9B7F7D19B8FC6369C6E2EAC5C1F3858A1E5E51342F5BCE10C3728" },
|
||||
{ name = "varasto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_json", "plinth"], otp_app = "varasto", source = "hex", outer_checksum = "0621E5BFD0B9B7F7D19B8FC6369C6E2EAC5C1F3858A1E5E51342F5BCE10C3728" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
|
|
|
@ -6,6 +6,34 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GeoTherminator</title>
|
||||
|
||||
<style>
|
||||
.pump {
|
||||
position: relative;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pump svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pump-btn:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pump-btn-loading:hover {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import "./build/dev/javascript/geo_therminator/priv/config.mjs";
|
||||
import { main } from "./build/dev/javascript/geo_therminator/geo_t/web.mjs";
|
||||
|
|
3
src/bitwise_ffi.mjs
Normal file
3
src/bitwise_ffi.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function band(a, b) {
|
||||
return a & b;
|
||||
}
|
2
src/geo_t/helpers/timers.gleam
Normal file
2
src/geo_t/helpers/timers.gleam
Normal file
|
@ -0,0 +1,2 @@
|
|||
@external(javascript, "../../timers_ffi.mjs", "setTimeout")
|
||||
pub fn set_timeout(callback: fn() -> a, delay: Int) -> Nil
|
8
src/geo_t/pump_api/api.gleam
Normal file
8
src/geo_t/pump_api/api.gleam
Normal file
|
@ -0,0 +1,8 @@
|
|||
import geo_t/azure/b2c.{B2CError}
|
||||
|
||||
pub type ApiError {
|
||||
ApiRequestFailed
|
||||
NotOkResponse
|
||||
InvalidData(msg: String)
|
||||
AuthError(inner: B2CError)
|
||||
}
|
|
@ -2,7 +2,6 @@ import gleam/http/request
|
|||
import gleam/json
|
||||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
import gleam/javascript/promise.{Promise}
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
|
@ -11,16 +10,12 @@ import geo_t/pump_api/auth/installation_info.{InstallationInfo}
|
|||
import geo_t/pump_api/http
|
||||
import geo_t/helpers/date
|
||||
import geo_t/helpers/parsing
|
||||
import geo_t/azure/b2c.{B2CError}
|
||||
import geo_t/azure/b2c
|
||||
import geo_t/helpers/promise as promise_helpers
|
||||
import geo_t/helpers/fetch as fetch_helpers
|
||||
import geo_t/config.{Config}
|
||||
|
||||
pub type ApiError {
|
||||
ApiRequestFailed
|
||||
NotOkResponse
|
||||
InvalidData(msg: String)
|
||||
AuthError(inner: B2CError)
|
||||
import geo_t/pump_api/api.{
|
||||
ApiError, ApiRequestFailed, AuthError, InvalidData, NotOkResponse,
|
||||
}
|
||||
|
||||
pub fn auth(
|
||||
|
@ -73,11 +68,11 @@ pub fn installation_info(
|
|||
use items <- promise.try_await(promise.resolve(parsing.data_get(
|
||||
data,
|
||||
"items",
|
||||
dynamic.list(of: dynamic.field("id", dynamic.int)),
|
||||
dynamic.list(of: installation_info.parse),
|
||||
InvalidData,
|
||||
)))
|
||||
|
||||
promise.resolve(Ok(list.map(items, fn(id) { InstallationInfo(id: id) })))
|
||||
promise.resolve(Ok(items))
|
||||
}
|
||||
|
||||
fn run_req(req: request.Request(String)) {
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
import gleam/result
|
||||
import gleam/dynamic.{Dynamic}
|
||||
|
||||
pub type InstallationInfo {
|
||||
InstallationInfo(id: Int)
|
||||
InstallationInfo(id: Int, name: String)
|
||||
}
|
||||
|
||||
pub fn parse(value: Dynamic) {
|
||||
use id <- result.try(dynamic.field("id", dynamic.int)(value))
|
||||
use name <- result.try(dynamic.field("name", dynamic.string)(value))
|
||||
|
||||
Ok(InstallationInfo(id, name))
|
||||
}
|
||||
|
|
19
src/geo_t/pump_api/device.gleam
Normal file
19
src/geo_t/pump_api/device.gleam
Normal file
|
@ -0,0 +1,19 @@
|
|||
import geo_t/helpers/date.{Date}
|
||||
|
||||
pub type Device {
|
||||
Device(
|
||||
id: Int,
|
||||
device_id: Int,
|
||||
is_online: Bool,
|
||||
last_online: Date,
|
||||
created_when: Date,
|
||||
mac_address: String,
|
||||
name: String,
|
||||
model: String,
|
||||
retailer_access: Int,
|
||||
)
|
||||
}
|
||||
|
||||
pub type Status {
|
||||
Status(heating_effect: Int, is_heating_effect_set_by_user: Bool)
|
||||
}
|
325
src/geo_t/pump_api/device/api.gleam
Normal file
325
src/geo_t/pump_api/device/api.gleam
Normal file
|
@ -0,0 +1,325 @@
|
|||
import gleam/http/request
|
||||
import gleam/http as gleam_http
|
||||
import gleam/string
|
||||
import gleam/int
|
||||
import gleam/dynamic
|
||||
import gleam/result
|
||||
import gleam/list
|
||||
import gleam/map
|
||||
import gleam/set
|
||||
import gleam/json
|
||||
import gleam/uri.{Uri}
|
||||
import gleam/javascript/promise
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
import geo_t/pump_api/auth/installation_info.{InstallationInfo}
|
||||
import geo_t/pump_api/device.{Device, Status}
|
||||
import geo_t/pump_api/device/register.{Register, RegisterCollection}
|
||||
import geo_t/pump_api/device/opstat.{OpStat}
|
||||
import geo_t/pump_api/http
|
||||
import geo_t/pump_api/api.{ApiError, InvalidData}
|
||||
import geo_t/config.{Config}
|
||||
import geo_t/helpers/parsing
|
||||
import geo_t/helpers/date
|
||||
|
||||
pub fn device_info(config: Config, user: User, installation: InstallationInfo) {
|
||||
let url =
|
||||
Uri(
|
||||
..config.api_device_url,
|
||||
path: string.replace(
|
||||
config.api_device_url.path,
|
||||
"{id}",
|
||||
int.to_string(installation.id),
|
||||
),
|
||||
)
|
||||
let assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let empty_req = request.set_body(raw_req, http.Empty)
|
||||
use data <- promise.map_try(http.run_json_req(http.authed_req(user, empty_req)))
|
||||
|
||||
use last_online <- result.then(parsing.data_get(
|
||||
data,
|
||||
"lastOnline",
|
||||
date.decode,
|
||||
InvalidData,
|
||||
))
|
||||
use created_when <- result.then(parsing.data_get(
|
||||
data,
|
||||
"createdWhen",
|
||||
date.decode,
|
||||
InvalidData,
|
||||
))
|
||||
use id <- result.then(parsing.data_get(data, "id", dynamic.int, InvalidData))
|
||||
use device_id <- result.then(parsing.data_get(
|
||||
data,
|
||||
"deviceId",
|
||||
dynamic.int,
|
||||
InvalidData,
|
||||
))
|
||||
use is_online <- result.then(parsing.data_get(
|
||||
data,
|
||||
"isOnline",
|
||||
dynamic.bool,
|
||||
InvalidData,
|
||||
))
|
||||
use mac_address <- result.then(parsing.data_get(
|
||||
data,
|
||||
"macAddress",
|
||||
dynamic.string,
|
||||
InvalidData,
|
||||
))
|
||||
use name <- result.then(parsing.data_get(
|
||||
data,
|
||||
"name",
|
||||
dynamic.string,
|
||||
InvalidData,
|
||||
))
|
||||
use model <- result.then(parsing.data_get(
|
||||
data,
|
||||
"model",
|
||||
dynamic.string,
|
||||
InvalidData,
|
||||
))
|
||||
use retailer_access <- result.then(parsing.data_get(
|
||||
data,
|
||||
"retailerAccess",
|
||||
dynamic.int,
|
||||
InvalidData,
|
||||
))
|
||||
|
||||
Ok(Device(
|
||||
id: id,
|
||||
device_id: device_id,
|
||||
is_online: is_online,
|
||||
last_online: last_online,
|
||||
created_when: created_when,
|
||||
mac_address: mac_address,
|
||||
name: name,
|
||||
model: model,
|
||||
retailer_access: retailer_access,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn status(config: Config, user: User, device: Device) {
|
||||
let url =
|
||||
Uri(
|
||||
..config.api_device_status_url,
|
||||
path: string.replace(
|
||||
config.api_device_status_url.path,
|
||||
"{id}",
|
||||
int.to_string(device.id),
|
||||
),
|
||||
)
|
||||
let assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let empty_req = request.set_body(raw_req, http.Empty)
|
||||
use data <- promise.map_try(http.run_json_req(http.authed_req(user, empty_req)))
|
||||
|
||||
use heating_effect <- result.then(parsing.data_get(
|
||||
data,
|
||||
"heatingEffect",
|
||||
dynamic.int,
|
||||
InvalidData,
|
||||
))
|
||||
use is_heating_effect_set_by_user <- result.then(parsing.data_get(
|
||||
data,
|
||||
"isHeatingEffectSetByUser",
|
||||
dynamic.bool,
|
||||
InvalidData,
|
||||
))
|
||||
|
||||
Ok(Status(
|
||||
heating_effect: heating_effect,
|
||||
is_heating_effect_set_by_user: is_heating_effect_set_by_user,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn register_info(config: Config, user: User, device: Device) {
|
||||
let url =
|
||||
Uri(
|
||||
..config.api_device_register_url,
|
||||
path: string.replace(
|
||||
config.api_device_register_url.path,
|
||||
"{id}",
|
||||
int.to_string(device.id),
|
||||
),
|
||||
)
|
||||
let assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let empty_req = request.set_body(raw_req, http.Empty)
|
||||
use data <- promise.map_try(http.run_json_req(http.authed_req(user, empty_req)))
|
||||
|
||||
use registers <- result.then(
|
||||
dynamic.list(parse_register)(data)
|
||||
|> result.map_error(fn(err) {
|
||||
InvalidData("Unable to parse registers: " <> string.inspect(err))
|
||||
}),
|
||||
)
|
||||
let registers_map =
|
||||
registers
|
||||
|> list.map(fn(r) { #(r.name, r) })
|
||||
|> map.from_list()
|
||||
|
||||
use outdoor_temp <- result.then(get_register(
|
||||
registers_map,
|
||||
"REG_OUTDOOR_TEMPERATURE",
|
||||
))
|
||||
use supply_out <- result.then(get_register(registers_map, "REG_SUPPLY_LINE"))
|
||||
use supply_in <- result.then(get_register(
|
||||
registers_map,
|
||||
"REG_OPER_DATA_RETURN",
|
||||
))
|
||||
use desired_supply <- result.then(get_register(
|
||||
registers_map,
|
||||
"REG_DESIRED_SYS_SUPPLY_LINE_TEMP",
|
||||
))
|
||||
use brine_out <- result.then(get_register(registers_map, "REG_BRINE_OUT"))
|
||||
use brine_in <- result.then(get_register(registers_map, "REG_BRINE_IN"))
|
||||
use hot_water_temp <- result.then(get_register(
|
||||
registers_map,
|
||||
"REG_HOT_WATER_TEMPERATURE",
|
||||
))
|
||||
|
||||
Ok(RegisterCollection(
|
||||
outdoor_temp: outdoor_temp,
|
||||
supply_out: supply_out,
|
||||
supply_in: supply_in,
|
||||
desired_supply: desired_supply,
|
||||
brine_out: brine_out,
|
||||
brine_in: brine_in,
|
||||
hot_water_temp: hot_water_temp,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn opstat(config: Config, user: User, device: Device) {
|
||||
let url =
|
||||
Uri(
|
||||
..config.api_device_opstat_url,
|
||||
path: string.replace(
|
||||
config.api_device_opstat_url.path,
|
||||
"{id}",
|
||||
int.to_string(device.id),
|
||||
),
|
||||
)
|
||||
let assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let empty_req = request.set_body(raw_req, http.Empty)
|
||||
use data <- promise.map_try(http.run_json_req(http.authed_req(user, empty_req)))
|
||||
|
||||
use registers <- result.then(
|
||||
dynamic.list(parse_register)(data)
|
||||
|> result.map_error(fn(err) {
|
||||
InvalidData("Unable to parse registers: " <> string.inspect(err))
|
||||
}),
|
||||
)
|
||||
let registers_map =
|
||||
registers
|
||||
|> list.map(fn(r) { #(r.name, r) })
|
||||
|> map.from_list()
|
||||
|
||||
let priority_register =
|
||||
get_register(registers_map, "REG_OPERATIONAL_STATUS_PRIO1")
|
||||
let priority_register_fallback =
|
||||
get_register(registers_map, "REG_OPERATIONAL_STATUS_PRIORITY_BITMASK")
|
||||
|
||||
use priority <- result.then(case
|
||||
#(priority_register, priority_register_fallback)
|
||||
{
|
||||
#(Ok(data), _) -> Ok(opstat_map(data))
|
||||
#(_, Ok(data)) -> Ok(opstat_bitmask_map(config, data))
|
||||
_ ->
|
||||
Error(InvalidData(
|
||||
"Unable to parse opstat: " <> string.inspect(#(
|
||||
priority_register,
|
||||
priority_register_fallback,
|
||||
)),
|
||||
))
|
||||
})
|
||||
|
||||
Ok(priority)
|
||||
}
|
||||
|
||||
pub fn set_temp(config: Config, user: User, device: Device, temp: Int) {
|
||||
let url =
|
||||
Uri(
|
||||
..config.api_device_opstat_url,
|
||||
path: string.replace(
|
||||
config.api_device_opstat_url.path,
|
||||
"{id}",
|
||||
int.to_string(device.id),
|
||||
),
|
||||
)
|
||||
let assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let register_index = config.api_device_temp_set_reg_index
|
||||
let client_id = config.api_device_reg_set_client_id
|
||||
|
||||
let req =
|
||||
raw_req
|
||||
|> request.set_method(gleam_http.Post)
|
||||
|> request.set_body(http.Json(data: json.object([
|
||||
#("registerIndex", json.int(register_index)),
|
||||
#("clientUuid", json.string(client_id)),
|
||||
#("registerValue", json.int(temp)),
|
||||
])))
|
||||
|
||||
http.run_req(http.authed_req(user, req))
|
||||
}
|
||||
|
||||
fn parse_register(
|
||||
item: dynamic.Dynamic,
|
||||
) -> Result(Register, List(dynamic.DecodeError)) {
|
||||
use timestamp <- result.then(dynamic.field("timeStamp", date.decode)(item))
|
||||
use name <- result.then(dynamic.field("registerName", dynamic.string)(item))
|
||||
use value <- result.then(dynamic.field("registerValue", dynamic.int)(item))
|
||||
|
||||
Ok(Register(timestamp: timestamp, name: name, value: value))
|
||||
}
|
||||
|
||||
fn get_register(
|
||||
data: map.Map(String, Register),
|
||||
key: String,
|
||||
) -> Result(Register, ApiError) {
|
||||
map.get(data, key)
|
||||
|> result.replace_error(InvalidData(
|
||||
"Could not find " <> key <> " in data: " <> string.inspect(data),
|
||||
))
|
||||
}
|
||||
|
||||
fn opstat_map(register: Register) {
|
||||
let val = case register.value {
|
||||
1 -> opstat.HandOperated
|
||||
3 -> opstat.HotWater
|
||||
4 -> opstat.Heating
|
||||
5 -> opstat.ActiveCooling
|
||||
6 -> opstat.Pool
|
||||
7 -> opstat.AntiLegionella
|
||||
8 -> opstat.PassiveCooling
|
||||
98 -> opstat.Standby
|
||||
99 -> opstat.Idle
|
||||
100 -> opstat.Off
|
||||
_ -> opstat.Unknown
|
||||
}
|
||||
|
||||
set.new()
|
||||
|> set.insert(val)
|
||||
}
|
||||
|
||||
fn opstat_bitmask_map(config: Config, register: Register) {
|
||||
let priority_set: set.Set(OpStat) = set.new()
|
||||
|
||||
list.fold(
|
||||
map.to_list(config.api_opstat_bitmask_mapping),
|
||||
priority_set,
|
||||
fn(output, mapping) {
|
||||
let #(int, priority) = mapping
|
||||
|
||||
case band(register.value, int) {
|
||||
0 -> output
|
||||
_ -> set.insert(output, priority)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@external(javascript, "../../../bitwise_ffi.mjs", "band")
|
||||
fn band(i1 i1: Int, i2 i2: Int) -> Int
|
|
@ -10,4 +10,5 @@ pub type OpStat {
|
|||
Standby
|
||||
Idle
|
||||
Off
|
||||
Unknown
|
||||
}
|
||||
|
|
17
src/geo_t/pump_api/device/register.gleam
Normal file
17
src/geo_t/pump_api/device/register.gleam
Normal file
|
@ -0,0 +1,17 @@
|
|||
import geo_t/helpers/date.{Date}
|
||||
|
||||
pub type Register {
|
||||
Register(name: String, value: Int, timestamp: Date)
|
||||
}
|
||||
|
||||
pub type RegisterCollection {
|
||||
RegisterCollection(
|
||||
outdoor_temp: Register,
|
||||
supply_out: Register,
|
||||
supply_in: Register,
|
||||
desired_supply: Register,
|
||||
brine_out: Register,
|
||||
brine_in: Register,
|
||||
hot_water_temp: Register,
|
||||
)
|
||||
}
|
|
@ -1,6 +1,12 @@
|
|||
import gleam/result
|
||||
import gleam/http/request
|
||||
import gleam/json.{Json}
|
||||
import gleam/dynamic
|
||||
import gleam/javascript/promise
|
||||
import geo_t/helpers/fetch
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
import geo_t/pump_api/api
|
||||
import geo_t/helpers/promise as promise_helpers
|
||||
|
||||
pub type Body {
|
||||
Empty
|
||||
|
@ -29,3 +35,28 @@ pub fn req(r: ApiRequest) -> request.Request(String) {
|
|||
|> request.set_body(json.to_string(data))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_req(req: request.Request(String)) {
|
||||
use resp <- promise.try_await(
|
||||
req
|
||||
|> fetch.log_send()
|
||||
|> promise_helpers.replace_error(api.ApiRequestFailed),
|
||||
)
|
||||
|
||||
case resp.status {
|
||||
200 -> promise.resolve(Ok(resp.body))
|
||||
_ -> promise.resolve(Error(api.NotOkResponse))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_json_req(req: request.Request(String)) {
|
||||
use body <- promise.try_await(run_req(req))
|
||||
|
||||
promise.resolve(
|
||||
body
|
||||
|> json.decode(using: dynamic.dynamic)
|
||||
|> result.replace_error(api.InvalidData(
|
||||
"Could not decode response as JSON.",
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import plinth/javascript/storage.{Storage}
|
|||
import geo_t/config.{Config}
|
||||
import geo_t/web/login_view
|
||||
import geo_t/web/installations_view
|
||||
import geo_t/web/pump_view
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
import geo_t/pump_api/auth/api.{ApiError}
|
||||
import geo_t/pump_api/api.{ApiError}
|
||||
import geo_t/pump_api/auth/api as auth_api
|
||||
import geo_t/web/auth.{AuthInfo, AuthInfoStorage}
|
||||
|
||||
pub fn main() {
|
||||
|
@ -26,6 +28,7 @@ pub type Model {
|
|||
local_storage: Storage,
|
||||
login: login_view.Model,
|
||||
installations: installations_view.Model,
|
||||
pump: Option(pump_view.Model),
|
||||
auth_storage: AuthInfoStorage,
|
||||
auth: Option(AuthInfo),
|
||||
logging_in: Bool,
|
||||
|
@ -37,6 +40,7 @@ pub type Msg {
|
|||
LoginView(login_view.Msg)
|
||||
LoginResult(Result(User, ApiError))
|
||||
InstallationsView(installations_view.Msg)
|
||||
PumpView(pump_view.Msg)
|
||||
}
|
||||
|
||||
fn init(_) {
|
||||
|
@ -48,18 +52,28 @@ fn init(_) {
|
|||
Error(_) -> None
|
||||
}
|
||||
|
||||
let #(inst_model, inst_effect) = case auth_info {
|
||||
Some(info) ->
|
||||
update_installations(
|
||||
installations_view.init(),
|
||||
installations_view.LoadInstallations(config, info.user),
|
||||
)
|
||||
None -> #(installations_view.init(), effect.none())
|
||||
}
|
||||
|
||||
#(
|
||||
Model(
|
||||
config: config,
|
||||
login: login_view.init(),
|
||||
installations: installations_view.init(),
|
||||
installations: inst_model,
|
||||
pump: None,
|
||||
local_storage: local,
|
||||
auth_storage: auth_storage,
|
||||
auth: auth_info,
|
||||
logging_in: False,
|
||||
logged_in: option.is_some(auth_info),
|
||||
),
|
||||
effect.none(),
|
||||
inst_effect,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -87,7 +101,7 @@ fn update(model: Model, msg: Msg) {
|
|||
|
||||
let #(inst_model, inst_effect) =
|
||||
update_installations(
|
||||
model,
|
||||
model.installations,
|
||||
installations_view.LoadInstallations(model.config, user),
|
||||
)
|
||||
|
||||
|
@ -117,10 +131,32 @@ fn update(model: Model, msg: Msg) {
|
|||
effect.none(),
|
||||
)
|
||||
|
||||
InstallationsView(installations_view.ViewInstallation(installation)) -> {
|
||||
let assert Some(auth) = model.auth
|
||||
let #(pump_model, pump_effect) =
|
||||
pump_view.init(model.config, auth.user, installation)
|
||||
|
||||
#(
|
||||
Model(..model, pump: Some(pump_model)),
|
||||
effect.map(pump_effect, PumpView),
|
||||
)
|
||||
}
|
||||
|
||||
InstallationsView(msg) -> {
|
||||
let #(new_model, new_effect) = update_installations(model, msg)
|
||||
let #(new_model, new_effect) =
|
||||
update_installations(model.installations, msg)
|
||||
#(Model(..model, installations: new_model), new_effect)
|
||||
}
|
||||
|
||||
PumpView(msg) -> {
|
||||
case model.pump {
|
||||
Some(pump) -> {
|
||||
let #(new_pump, pump_effect) = update_pump(pump, msg)
|
||||
#(Model(..model, pump: Some(new_pump)), pump_effect)
|
||||
}
|
||||
None -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,9 +168,16 @@ fn view(model: Model) {
|
|||
False ->
|
||||
login_view.view(model.login)
|
||||
|> element.map(LoginView)
|
||||
True ->
|
||||
True -> {
|
||||
case model.pump {
|
||||
None ->
|
||||
installations_view.view(model.installations)
|
||||
|> element.map(InstallationsView)
|
||||
Some(pump) ->
|
||||
pump_view.view(pump)
|
||||
|> element.map(PumpView)
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
|
@ -143,16 +186,31 @@ fn view(model: Model) {
|
|||
fn login(model: Model) -> Effect(Msg) {
|
||||
use dispatch <- effect.from()
|
||||
|
||||
api.auth(model.config, model.login.username, model.login.password)
|
||||
auth_api.auth(model.config, model.login.username, model.login.password)
|
||||
|> promise.map(LoginResult)
|
||||
|> promise.tap(dispatch)
|
||||
|
||||
Nil
|
||||
}
|
||||
|
||||
fn update_installations(model: Model, msg: installations_view.Msg) {
|
||||
let #(new_model, new_effect) =
|
||||
installations_view.update(model.installations, msg)
|
||||
let new_effect = effect.map(new_effect, fn(msg) { InstallationsView(msg) })
|
||||
fn update_installations(
|
||||
model: installations_view.Model,
|
||||
msg: installations_view.Msg,
|
||||
) {
|
||||
update_child(model, msg, installations_view.update, InstallationsView)
|
||||
}
|
||||
|
||||
fn update_pump(model: pump_view.Model, msg: pump_view.Msg) {
|
||||
update_child(model, msg, pump_view.update, PumpView)
|
||||
}
|
||||
|
||||
fn update_child(
|
||||
model: a,
|
||||
msg: b,
|
||||
updater: fn(a, b) -> #(a, Effect(b)),
|
||||
mapper: fn(b) -> Msg,
|
||||
) {
|
||||
let #(new_model, new_effect) = updater(model, msg)
|
||||
let new_effect = effect.map(new_effect, mapper)
|
||||
#(new_model, new_effect)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import lustre/event
|
|||
import lustre/effect.{Effect}
|
||||
import geo_t/config.{Config}
|
||||
import geo_t/pump_api/auth/installation_info.{InstallationInfo}
|
||||
import geo_t/pump_api/auth/api.{ApiError}
|
||||
import geo_t/pump_api/auth/api as auth_api
|
||||
import geo_t/pump_api/api.{ApiError}
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
|
||||
type InstallationData =
|
||||
|
@ -26,6 +27,7 @@ pub fn init() {
|
|||
pub type Msg {
|
||||
LoadInstallations(Config, User)
|
||||
InstallationsResult(Result(List(InstallationInfo), ApiError))
|
||||
ViewInstallation(InstallationInfo)
|
||||
}
|
||||
|
||||
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
|
@ -39,6 +41,8 @@ pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
|||
Model(loading: False, installations: data),
|
||||
effect.none(),
|
||||
)
|
||||
|
||||
ViewInstallation(_) -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,8 +59,16 @@ pub fn view(model: Model) -> Element(Msg) {
|
|||
installations,
|
||||
fn(installation) {
|
||||
html.button(
|
||||
[attribute.type_("button")],
|
||||
[text(int.to_string(installation.id))],
|
||||
[
|
||||
attribute.type_("button"),
|
||||
event.on_click(ViewInstallation(installation)),
|
||||
],
|
||||
[
|
||||
text(installation.name),
|
||||
text(" ("),
|
||||
text(int.to_string(installation.id)),
|
||||
text(")"),
|
||||
],
|
||||
)
|
||||
},
|
||||
),
|
||||
|
@ -74,7 +86,7 @@ pub fn view(model: Model) -> Element(Msg) {
|
|||
fn load_installations(config: Config, user: User) {
|
||||
use dispatch <- effect.from()
|
||||
|
||||
api.installation_info(config, user)
|
||||
auth_api.installation_info(config, user)
|
||||
|> promise.map(InstallationsResult)
|
||||
|> promise.tap(dispatch)
|
||||
|
||||
|
|
926
src/geo_t/web/pump_view.gleam
Normal file
926
src/geo_t/web/pump_view.gleam
Normal file
|
@ -0,0 +1,926 @@
|
|||
import gleam/list
|
||||
import gleam/int
|
||||
import gleam/option.{None, Option, Some}
|
||||
import gleam/set.{Set}
|
||||
import gleam/javascript/promise
|
||||
import lustre/element.{Element, text}
|
||||
import lustre/element/html
|
||||
import lustre/attribute.{attribute}
|
||||
import lustre/effect.{Effect}
|
||||
import lustre/element/svg
|
||||
import geo_t/helpers/timers
|
||||
import geo_t/config.{Config}
|
||||
import geo_t/pump_api/auth/installation_info.{InstallationInfo}
|
||||
import geo_t/pump_api/device.{Device}
|
||||
import geo_t/pump_api/device/opstat
|
||||
import geo_t/pump_api/device/register.{RegisterCollection}
|
||||
import geo_t/pump_api/device/api as device_api
|
||||
import geo_t/pump_api/api.{ApiError}
|
||||
import geo_t/pump_api/auth/user.{User}
|
||||
|
||||
const svg_ns = "http://www.w3.org/2000/svg"
|
||||
|
||||
pub type UpdateResult {
|
||||
Status(Result(device.Status, ApiError))
|
||||
Registers(Result(RegisterCollection, ApiError))
|
||||
OpStat(Result(Set(opstat.OpStat), ApiError))
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(
|
||||
loading: Bool,
|
||||
config: Config,
|
||||
user: User,
|
||||
installation: InstallationInfo,
|
||||
device: Option(Device),
|
||||
status: Option(device.Status),
|
||||
registers: Option(RegisterCollection),
|
||||
opstat: Set(opstat.OpStat),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
DeviceLoadResult(Result(Device, ApiError))
|
||||
StartUpdate(Device)
|
||||
UpdateResults(List(UpdateResult))
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
config: Config,
|
||||
user: User,
|
||||
installation: InstallationInfo,
|
||||
) -> #(Model, Effect(Msg)) {
|
||||
#(
|
||||
Model(
|
||||
loading: True,
|
||||
config: config,
|
||||
user: user,
|
||||
installation: installation,
|
||||
device: None,
|
||||
status: None,
|
||||
registers: None,
|
||||
opstat: set.from_list([opstat.Unknown]),
|
||||
),
|
||||
load_device(config, user, installation),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
DeviceLoadResult(Ok(device)) -> #(
|
||||
Model(..model, loading: False, device: Some(device)),
|
||||
update_state(model.config, model.user, device),
|
||||
)
|
||||
|
||||
StartUpdate(device) -> #(
|
||||
Model(..model, loading: True, device: Some(device)),
|
||||
effect.none(),
|
||||
)
|
||||
|
||||
UpdateResults(results) -> #(
|
||||
list.fold(
|
||||
results,
|
||||
Model(..model, loading: False),
|
||||
fn(m, result) {
|
||||
case result {
|
||||
Status(Ok(status)) -> Model(..m, status: Some(status))
|
||||
Registers(Ok(registers)) -> Model(..m, registers: Some(registers))
|
||||
OpStat(Ok(opstat)) -> Model(..m, opstat: opstat)
|
||||
}
|
||||
},
|
||||
),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: Model) -> Element(Msg) {
|
||||
html.div(
|
||||
[attribute.class("pump")],
|
||||
[
|
||||
html.div(
|
||||
[attribute.class("pump-loading-info")],
|
||||
[
|
||||
case model.loading {
|
||||
True -> text("…")
|
||||
False -> text(" ")
|
||||
},
|
||||
],
|
||||
),
|
||||
html.svg(
|
||||
[
|
||||
attribute("xmlns", svg_ns),
|
||||
attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"),
|
||||
attribute("version", "1.1"),
|
||||
attribute("width", "484px"),
|
||||
attribute("height", "665px"),
|
||||
attribute("viewBox", "-0.5 -0.5 484 665"),
|
||||
attribute("style", "background-color: rgb(255, 255, 255);"),
|
||||
],
|
||||
[
|
||||
svg.defs(
|
||||
[],
|
||||
[
|
||||
svg.linear_gradient(
|
||||
[
|
||||
attribute("x1", "0%"),
|
||||
attribute("y1", "0%"),
|
||||
attribute("x2", "0%"),
|
||||
attribute("y2", "100%"),
|
||||
attribute("id", "mx-gradient-f5f5f5-1-b3b3b3-1-s-0"),
|
||||
],
|
||||
[
|
||||
svg.stop([
|
||||
attribute("offset", "0%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(245, 245, 245); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
svg.stop([
|
||||
attribute("offset", "100%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(179, 179, 179); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
svg.linear_gradient(
|
||||
[
|
||||
attribute("x1", "0%"),
|
||||
attribute("y1", "0%"),
|
||||
attribute("x2", "0%"),
|
||||
attribute("y2", "100%"),
|
||||
attribute("id", "mx-gradient-dae8fc-1-7ea6e0-1-s-0"),
|
||||
],
|
||||
[
|
||||
svg.stop([
|
||||
attribute("offset", "0%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(218, 232, 252); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
svg.stop([
|
||||
attribute("offset", "100%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(126, 166, 224); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
svg.linear_gradient(
|
||||
[
|
||||
attribute("x1", "0%"),
|
||||
attribute("y1", "0%"),
|
||||
attribute("x2", "0%"),
|
||||
attribute("y2", "100%"),
|
||||
attribute("id", "mx-gradient-f8cecc-1-ea6b66-1-s-0"),
|
||||
],
|
||||
[
|
||||
svg.stop([
|
||||
attribute("offset", "0%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(248, 206, 204); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
svg.stop([
|
||||
attribute("offset", "100%"),
|
||||
attribute(
|
||||
"style",
|
||||
"stop-color: rgb(234, 107, 102); stop-opacity: 1;",
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.g(
|
||||
[],
|
||||
[
|
||||
svg.rect([
|
||||
attribute("x", "0"),
|
||||
attribute("y", "144"),
|
||||
attribute("width", "200"),
|
||||
attribute("height", "520"),
|
||||
attribute("fill", "url(#mx-gradient-f5f5f5-1-b3b3b3-1-s-0)"),
|
||||
attribute("stroke", "#666666"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "60"),
|
||||
attribute("y", "194"),
|
||||
attribute("width", "80"),
|
||||
attribute("height", "80"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "90"),
|
||||
attribute("y", "464"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "185"),
|
||||
attribute("y", "498"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.outdoor_temp.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "10"),
|
||||
attribute("y", "468"),
|
||||
attribute("width", "30"),
|
||||
attribute("height", "40"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 30 488 L 53.63 488"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 58.88 488 L 51.88 491.5 L 53.63 488 L 51.88 484.5 Z",
|
||||
),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
case set.contains(model.opstat, opstat.Heating) {
|
||||
False -> text("")
|
||||
True ->
|
||||
svg.ellipse([
|
||||
attribute("cx", "172"),
|
||||
attribute("cy", "289"),
|
||||
attribute("rx", "15"),
|
||||
attribute("ry", "15"),
|
||||
attribute("fill", "#ffff88"),
|
||||
attribute("stroke", "#36393d"),
|
||||
attribute("pointer-events", "none"),
|
||||
])
|
||||
},
|
||||
case set.contains(model.opstat, opstat.HotWater) {
|
||||
False -> text("")
|
||||
True ->
|
||||
svg.ellipse([
|
||||
attribute("cx", "172"),
|
||||
attribute("cy", "169"),
|
||||
attribute("rx", "15"),
|
||||
attribute("ry", "15"),
|
||||
attribute("fill", "#ffff88"),
|
||||
attribute("stroke", "#36393d"),
|
||||
attribute("pointer-events", "none"),
|
||||
])
|
||||
},
|
||||
svg.path([
|
||||
attribute("d", "M 10 344 Q 10 344 53.63 344"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 58.88 344 L 51.88 347.5 L 53.63 344 L 51.88 340.5 Z",
|
||||
),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "90"),
|
||||
attribute("y", "320"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "185"),
|
||||
attribute("y", "354"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.status,
|
||||
fn(s) { s.heating_effect },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("id", "dec-tmp-rect"),
|
||||
attribute(
|
||||
"class",
|
||||
"pump-btn " <> case set_temp_active(model) {
|
||||
True -> "pump-btn-loading"
|
||||
False -> ""
|
||||
},
|
||||
),
|
||||
attribute("x", "25"),
|
||||
attribute("y", "384"),
|
||||
attribute("width", "60"),
|
||||
attribute("height", "60"),
|
||||
attribute("rx", "9"),
|
||||
attribute("ry", "9"),
|
||||
attribute(
|
||||
"fill",
|
||||
case set_temp_active(model) {
|
||||
True -> "#ccc"
|
||||
False -> "url(#mx-gradient-dae8fc-1-7ea6e0-1-s-0)"
|
||||
},
|
||||
),
|
||||
attribute("stroke", "#6c8ebf"),
|
||||
attribute("phx-click", "dec_temp"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("id", "dec-tmp-text"),
|
||||
attribute(
|
||||
"class",
|
||||
"pump-btn " <> case set_temp_active(model) {
|
||||
True -> "pump-btn-loading"
|
||||
False -> ""
|
||||
},
|
||||
),
|
||||
attribute("x", "55"),
|
||||
attribute("y", "424"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "middle"),
|
||||
attribute("phx-click", "dec_temp"),
|
||||
],
|
||||
[text("-")],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("id", "inc-tmp-rect"),
|
||||
attribute(
|
||||
"class",
|
||||
"pump-btn " <> case set_temp_active(model) {
|
||||
True -> "pump-btn-loading"
|
||||
False -> ""
|
||||
},
|
||||
),
|
||||
attribute("x", "115"),
|
||||
attribute("y", "384"),
|
||||
attribute("width", "60"),
|
||||
attribute("height", "60"),
|
||||
attribute("rx", "9"),
|
||||
attribute("ry", "9"),
|
||||
attribute(
|
||||
"fill",
|
||||
case set_temp_active(model) {
|
||||
True -> "#ccc"
|
||||
False -> "url(#mx-gradient-f8cecc-1-ea6b66-1-s-0)"
|
||||
},
|
||||
),
|
||||
attribute("stroke", "#b85450"),
|
||||
attribute("phx-click", "inc_temp"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("id", "inc-tmp-text"),
|
||||
attribute(
|
||||
"class",
|
||||
"pump-btn " <> case set_temp_active(model) {
|
||||
True -> "pump-btn-loading"
|
||||
False -> ""
|
||||
},
|
||||
),
|
||||
attribute("x", "145"),
|
||||
attribute("y", "424"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "middle"),
|
||||
attribute("phx-click", "inc_temp"),
|
||||
],
|
||||
[text("+")],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.path([
|
||||
attribute("d", "M 50 144 L 50 24 L 480 24"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#b85450"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 150 144 L 150 84 L 480 84"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#6c8ebf"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 200 353 L 310 353 L 310 323 L 480 323"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#6c8ebf"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 200 223 L 310 223 L 310 263 L 480 263"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#b85450"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 200 614 L 310 614 L 310 584 L 480 584"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#b85450"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 200 484 L 310 484 L 310 524 L 480 524"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "#6c8ebf"),
|
||||
attribute("stroke-width", "4"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "368"),
|
||||
attribute("y", "560"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "463"),
|
||||
attribute("y", "594"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.brine_in.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "368"),
|
||||
attribute("y", "500"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "463"),
|
||||
attribute("y", "534"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.brine_out.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "368"),
|
||||
attribute("y", "298"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "463"),
|
||||
attribute("y", "332"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.supply_in.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "368"),
|
||||
attribute("y", "239"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "463"),
|
||||
attribute("y", "273"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.supply_out.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "168"),
|
||||
attribute("y", "32"),
|
||||
attribute("width", "97"),
|
||||
attribute("height", "48"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "none"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.g(
|
||||
[attribute("transform", "translate(-0.5 -0.5)")],
|
||||
[
|
||||
element.namespaced(
|
||||
svg_ns,
|
||||
"text",
|
||||
[
|
||||
attribute("x", "263"),
|
||||
attribute("y", "66"),
|
||||
attribute("fill", "rgba(0, 0, 0, 1)"),
|
||||
attribute("font-family", "Helvetica"),
|
||||
attribute("font-size", "34px"),
|
||||
attribute("text-anchor", "end"),
|
||||
],
|
||||
[
|
||||
text(
|
||||
var(
|
||||
model.registers,
|
||||
fn(r) { r.hot_water_temp.value },
|
||||
int.to_string,
|
||||
) <> "°C",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
svg.rect([
|
||||
attribute("x", "269"),
|
||||
attribute("y", "284"),
|
||||
attribute("width", "40"),
|
||||
attribute("height", "10"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "210"),
|
||||
attribute("y", "314"),
|
||||
attribute("width", "40"),
|
||||
attribute("height", "10"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "210"),
|
||||
attribute("y", "284"),
|
||||
attribute("width", "40"),
|
||||
attribute("height", "10"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.rect([
|
||||
attribute("x", "220"),
|
||||
attribute("y", "274"),
|
||||
attribute("width", "80"),
|
||||
attribute("height", "60"),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 230 326.5 L 230 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 240 326.5 L 240 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 250 326.5 L 250 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 260 326.5 L 260 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 270 326.5 L 270 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 280 326.5 L 280 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 290 326.5 L 290 281.5"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 230 264 Q 220 254 230 249 Q 240 244 230 234"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 250 264 Q 240 254 250 249 Q 260 244 250 234"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 270 264 Q 260 254 270 249 Q 280 244 270 234"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute("d", "M 290 264 Q 280 254 290 249 Q 300 244 290 234"),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 70 60 L 140 60 L 140 80 L 90 80 L 90 100 L 70 100 Z",
|
||||
),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 105 35 L 125 35 L 125 45 L 115 45 L 115 65 L 105 65 Z",
|
||||
),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("transform", "rotate(90,115,50)"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 80 102 L 84.71 115.33 C 84.9 115.87 85 116.43 85 117 C 85 118.33 84.47 119.6 83.54 120.54 C 82.6 121.47 81.33 122 80 122 C 78.67 122 77.4 121.47 76.46 120.54 C 75.53 119.6 75 118.33 75 117 C 75 116.43 75.1 115.87 75.29 115.33 Z",
|
||||
),
|
||||
attribute("fill", "#dae8fc"),
|
||||
attribute("stroke", "#6c8ebf"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 235 506 C 235 497.72 245.07 491 257.5 491 C 263.47 491 269.19 492.58 273.41 495.39 C 277.63 498.21 280 502.02 280 506 L 280 592 C 280 600.28 269.93 607 257.5 607 C 245.07 607 235 600.28 235 592 Z",
|
||||
),
|
||||
attribute("fill", "rgba(255, 255, 255, 1)"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
svg.path([
|
||||
attribute(
|
||||
"d",
|
||||
"M 280 506 C 280 514.28 269.93 521 257.5 521 C 245.07 521 235 514.28 235 506",
|
||||
),
|
||||
attribute("fill", "none"),
|
||||
attribute("stroke", "rgba(0, 0, 0, 1)"),
|
||||
attribute("stroke-miterlimit", "10"),
|
||||
attribute("pointer-events", "none"),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn var(
|
||||
source: Option(a),
|
||||
mapper: fn(a) -> b,
|
||||
stringifier: fn(b) -> String,
|
||||
) -> String {
|
||||
case source {
|
||||
Some(data) ->
|
||||
data
|
||||
|> mapper()
|
||||
|> stringifier()
|
||||
None -> "--"
|
||||
}
|
||||
}
|
||||
|
||||
fn set_temp_active(model: Model) -> Bool {
|
||||
case model.status {
|
||||
Some(status) -> status.is_heating_effect_set_by_user
|
||||
|
||||
None -> False
|
||||
}
|
||||
}
|
||||
|
||||
fn load_device(config: Config, user: User, installation: InstallationInfo) {
|
||||
use dispatch <- effect.from()
|
||||
|
||||
device_api.device_info(config, user, installation)
|
||||
|> promise.map(DeviceLoadResult)
|
||||
|> promise.tap(dispatch)
|
||||
|
||||
Nil
|
||||
}
|
||||
|
||||
fn update_state(config: Config, user: User, device: Device) {
|
||||
use dispatch <- effect.from()
|
||||
dispatch(StartUpdate(device))
|
||||
do_update_state(dispatch, config, user, device)
|
||||
Nil
|
||||
}
|
||||
|
||||
fn do_update_state(
|
||||
dispatch: fn(Msg) -> Nil,
|
||||
config: Config,
|
||||
user: User,
|
||||
device: Device,
|
||||
) {
|
||||
timers.set_timeout(
|
||||
fn() {
|
||||
dispatch(StartUpdate(device))
|
||||
do_update_state(dispatch, config, user, device)
|
||||
},
|
||||
config.api_refresh,
|
||||
)
|
||||
|
||||
[
|
||||
device_api.status(config, user, device)
|
||||
|> promise.map(Status),
|
||||
device_api.register_info(config, user, device)
|
||||
|> promise.map(Registers),
|
||||
device_api.opstat(config, user, device)
|
||||
|> promise.map(OpStat),
|
||||
]
|
||||
|> promise.await_list()
|
||||
|> promise.map(UpdateResults)
|
||||
|> promise.tap(dispatch)
|
||||
|
||||
Nil
|
||||
}
|
3
src/timers_ffi.mjs
Normal file
3
src/timers_ffi.mjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function setTimeout(callback, delay) {
|
||||
globalThis.setTimeout(callback, delay);
|
||||
}
|
Loading…
Reference in a new issue