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
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
packages = [
|
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_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_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_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 = "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 = "lustre", version = "3.0.5", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "6DD8FC3238623EF3CEC425780596C13FC7F7FFD53B3E44073669B61E6B5E4F02" },
|
||||||
{ name = "plinth", version = "0.1.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "E40A48FAA3AB9410803AB937BE620692D86B7ABB46459A83E8C674B82CFFD05B" },
|
{ 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 = "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]
|
[requirements]
|
||||||
|
|
|
@ -6,6 +6,34 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>GeoTherminator</title>
|
<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">
|
<script type="module">
|
||||||
import "./build/dev/javascript/geo_therminator/priv/config.mjs";
|
import "./build/dev/javascript/geo_therminator/priv/config.mjs";
|
||||||
import { main } from "./build/dev/javascript/geo_therminator/geo_t/web.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/json
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
import gleam/list
|
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import gleam/javascript/promise.{Promise}
|
import gleam/javascript/promise.{Promise}
|
||||||
import geo_t/pump_api/auth/user.{User}
|
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/pump_api/http
|
||||||
import geo_t/helpers/date
|
import geo_t/helpers/date
|
||||||
import geo_t/helpers/parsing
|
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/promise as promise_helpers
|
||||||
import geo_t/helpers/fetch as fetch_helpers
|
import geo_t/helpers/fetch as fetch_helpers
|
||||||
import geo_t/config.{Config}
|
import geo_t/config.{Config}
|
||||||
|
import geo_t/pump_api/api.{
|
||||||
pub type ApiError {
|
ApiError, ApiRequestFailed, AuthError, InvalidData, NotOkResponse,
|
||||||
ApiRequestFailed
|
|
||||||
NotOkResponse
|
|
||||||
InvalidData(msg: String)
|
|
||||||
AuthError(inner: B2CError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth(
|
pub fn auth(
|
||||||
|
@ -73,11 +68,11 @@ pub fn installation_info(
|
||||||
use items <- promise.try_await(promise.resolve(parsing.data_get(
|
use items <- promise.try_await(promise.resolve(parsing.data_get(
|
||||||
data,
|
data,
|
||||||
"items",
|
"items",
|
||||||
dynamic.list(of: dynamic.field("id", dynamic.int)),
|
dynamic.list(of: installation_info.parse),
|
||||||
InvalidData,
|
InvalidData,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
promise.resolve(Ok(list.map(items, fn(id) { InstallationInfo(id: id) })))
|
promise.resolve(Ok(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_req(req: request.Request(String)) {
|
fn run_req(req: request.Request(String)) {
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
|
import gleam/result
|
||||||
|
import gleam/dynamic.{Dynamic}
|
||||||
|
|
||||||
pub type InstallationInfo {
|
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
|
Standby
|
||||||
Idle
|
Idle
|
||||||
Off
|
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/http/request
|
||||||
import gleam/json.{Json}
|
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/auth/user.{User}
|
||||||
|
import geo_t/pump_api/api
|
||||||
|
import geo_t/helpers/promise as promise_helpers
|
||||||
|
|
||||||
pub type Body {
|
pub type Body {
|
||||||
Empty
|
Empty
|
||||||
|
@ -29,3 +35,28 @@ pub fn req(r: ApiRequest) -> request.Request(String) {
|
||||||
|> request.set_body(json.to_string(data))
|
|> 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/config.{Config}
|
||||||
import geo_t/web/login_view
|
import geo_t/web/login_view
|
||||||
import geo_t/web/installations_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/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}
|
import geo_t/web/auth.{AuthInfo, AuthInfoStorage}
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
|
@ -26,6 +28,7 @@ pub type Model {
|
||||||
local_storage: Storage,
|
local_storage: Storage,
|
||||||
login: login_view.Model,
|
login: login_view.Model,
|
||||||
installations: installations_view.Model,
|
installations: installations_view.Model,
|
||||||
|
pump: Option(pump_view.Model),
|
||||||
auth_storage: AuthInfoStorage,
|
auth_storage: AuthInfoStorage,
|
||||||
auth: Option(AuthInfo),
|
auth: Option(AuthInfo),
|
||||||
logging_in: Bool,
|
logging_in: Bool,
|
||||||
|
@ -37,6 +40,7 @@ pub type Msg {
|
||||||
LoginView(login_view.Msg)
|
LoginView(login_view.Msg)
|
||||||
LoginResult(Result(User, ApiError))
|
LoginResult(Result(User, ApiError))
|
||||||
InstallationsView(installations_view.Msg)
|
InstallationsView(installations_view.Msg)
|
||||||
|
PumpView(pump_view.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(_) {
|
fn init(_) {
|
||||||
|
@ -48,18 +52,28 @@ fn init(_) {
|
||||||
Error(_) -> None
|
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(
|
Model(
|
||||||
config: config,
|
config: config,
|
||||||
login: login_view.init(),
|
login: login_view.init(),
|
||||||
installations: installations_view.init(),
|
installations: inst_model,
|
||||||
|
pump: None,
|
||||||
local_storage: local,
|
local_storage: local,
|
||||||
auth_storage: auth_storage,
|
auth_storage: auth_storage,
|
||||||
auth: auth_info,
|
auth: auth_info,
|
||||||
logging_in: False,
|
logging_in: False,
|
||||||
logged_in: option.is_some(auth_info),
|
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) =
|
let #(inst_model, inst_effect) =
|
||||||
update_installations(
|
update_installations(
|
||||||
model,
|
model.installations,
|
||||||
installations_view.LoadInstallations(model.config, user),
|
installations_view.LoadInstallations(model.config, user),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -117,10 +131,32 @@ fn update(model: Model, msg: Msg) {
|
||||||
effect.none(),
|
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) -> {
|
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)
|
#(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 ->
|
False ->
|
||||||
login_view.view(model.login)
|
login_view.view(model.login)
|
||||||
|> element.map(LoginView)
|
|> element.map(LoginView)
|
||||||
True ->
|
True -> {
|
||||||
installations_view.view(model.installations)
|
case model.pump {
|
||||||
|> element.map(InstallationsView)
|
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) {
|
fn login(model: Model) -> Effect(Msg) {
|
||||||
use dispatch <- effect.from()
|
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.map(LoginResult)
|
||||||
|> promise.tap(dispatch)
|
|> promise.tap(dispatch)
|
||||||
|
|
||||||
Nil
|
Nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_installations(model: Model, msg: installations_view.Msg) {
|
fn update_installations(
|
||||||
let #(new_model, new_effect) =
|
model: installations_view.Model,
|
||||||
installations_view.update(model.installations, msg)
|
msg: installations_view.Msg,
|
||||||
let new_effect = effect.map(new_effect, fn(msg) { InstallationsView(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)
|
#(new_model, new_effect)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ import lustre/event
|
||||||
import lustre/effect.{Effect}
|
import lustre/effect.{Effect}
|
||||||
import geo_t/config.{Config}
|
import geo_t/config.{Config}
|
||||||
import geo_t/pump_api/auth/installation_info.{InstallationInfo}
|
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}
|
import geo_t/pump_api/auth/user.{User}
|
||||||
|
|
||||||
type InstallationData =
|
type InstallationData =
|
||||||
|
@ -26,6 +27,7 @@ pub fn init() {
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
LoadInstallations(Config, User)
|
LoadInstallations(Config, User)
|
||||||
InstallationsResult(Result(List(InstallationInfo), ApiError))
|
InstallationsResult(Result(List(InstallationInfo), ApiError))
|
||||||
|
ViewInstallation(InstallationInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
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),
|
Model(loading: False, installations: data),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ViewInstallation(_) -> #(model, effect.none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +59,16 @@ pub fn view(model: Model) -> Element(Msg) {
|
||||||
installations,
|
installations,
|
||||||
fn(installation) {
|
fn(installation) {
|
||||||
html.button(
|
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) {
|
fn load_installations(config: Config, user: User) {
|
||||||
use dispatch <- effect.from()
|
use dispatch <- effect.from()
|
||||||
|
|
||||||
api.installation_info(config, user)
|
auth_api.installation_info(config, user)
|
||||||
|> promise.map(InstallationsResult)
|
|> promise.map(InstallationsResult)
|
||||||
|> promise.tap(dispatch)
|
|> 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