diff --git a/config/runtime.exs b/config/runtime.exs
index 6a7c95c..83628dc 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -77,6 +77,13 @@ config :geo_therminator,
api_device_temp_set_reg_index: 3,
api_device_reg_set_client_id: get_env("API_DEVICE_REG_SET_CLIENT_ID"),
api_refresh: 10_000,
+ b2c_client_id: get_env("B2C_CLIENT_ID", "09ea4903-9e95-45fe-ae1f-e3b7d32fa385"),
+ b2c_redirect_url: get_env("B2C_REDIRECT_URL", "https://online-genesis.thermia.se/login"),
+ b2c_auth_url:
+ get_env(
+ "B2C_AUTH_URL",
+ "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft.com/b2c_1a_signuporsigninonline"
+ ),
# Database directory for settings
db_dir:
diff --git a/lib/geo_therminator/pump_api/auth/tokens.ex b/lib/geo_therminator/pump_api/auth/tokens.ex
index af3f0d4..0d5da0a 100644
--- a/lib/geo_therminator/pump_api/auth/tokens.ex
+++ b/lib/geo_therminator/pump_api/auth/tokens.ex
@@ -8,11 +8,5 @@ defmodule GeoTherminator.PumpAPI.Auth.Tokens do
:refresh_token_expiry
])
- @type t ::
- record(:record,
- access_token: String.t(),
- access_token_expiry: DateTime.t(),
- refresh_token: String.t(),
- refresh_token_expiry: DateTime.t()
- )
+ @type t :: :pump_api@auth@tokens.tokens()
end
diff --git a/lib/geo_therminator/pump_api/auth/user.ex b/lib/geo_therminator/pump_api/auth/user.ex
index a09d577..8886958 100644
--- a/lib/geo_therminator/pump_api/auth/user.ex
+++ b/lib/geo_therminator/pump_api/auth/user.ex
@@ -3,5 +3,5 @@ defmodule GeoTherminator.PumpAPI.Auth.User do
Record.defrecord(:record, :user, [:tokens])
- @type t :: record(:record, tokens: GeoTherminator.PumpAPI.Auth.Tokens.t())
+ @type t :: :pump_api@auth@user.user()
end
diff --git a/lib/geo_therminator/pump_api/device/api.ex b/lib/geo_therminator/pump_api/device/api.ex
deleted file mode 100644
index 9436fb2..0000000
--- a/lib/geo_therminator/pump_api/device/api.ex
+++ /dev/null
@@ -1,154 +0,0 @@
-defmodule GeoTherminator.PumpAPI.Device.API do
- require GeoTherminator.PumpAPI.Auth.InstallationInfo
-
- alias GeoTherminator.PumpAPI.HTTP
- alias GeoTherminator.PumpAPI.Device
- alias GeoTherminator.PumpAPI.Auth
-
- @spec device_info(Auth.User.t(), Auth.InstallationInfo.t()) :: Device.t()
- def device_info(user, installation) do
- url =
- Application.get_env(:geo_therminator, :api_device_url)
- |> String.replace("{id}", to_string(Auth.InstallationInfo.record(installation, :id)))
-
- req = HTTP.authed_req(user, :get, url)
-
- {:ok, response} = Finch.request(req, HTTP)
- json = Jason.decode!(response.body)
-
- last_online = NaiveDateTime.from_iso8601!(json["lastOnline"])
- created_when = NaiveDateTime.from_iso8601!(json["createdWhen"])
-
- %Device{
- id: json["id"],
- device_id: json["deviceId"],
- is_online: json["isOnline"],
- last_online: last_online,
- created_when: created_when,
- mac_address: json["macAddress"],
- name: json["name"],
- model: json["model"],
- retailer_access: json["retailerAccess"]
- }
- end
-
- @spec status(Auth.User.t(), Device.t()) :: Device.Status.t()
- def status(user, device) do
- url =
- Application.get_env(:geo_therminator, :api_device_status_url)
- |> String.replace("{id}", to_string(device.id))
-
- req = HTTP.authed_req(user, :get, url)
-
- {:ok, response} = Finch.request(req, HTTP)
- json = Jason.decode!(response.body)
-
- %Device.Status{
- heating_effect: json["heatingEffect"],
- is_heating_effect_set_by_user: json["isHeatingEffectSetByUser"]
- }
- end
-
- @spec register_info(Auth.User.t(), Device.t()) :: Device.RegisterCollection.t()
- def register_info(user, device) do
- url =
- Application.get_env(:geo_therminator, :api_device_register_url)
- |> String.replace("{id}", to_string(device.id))
-
- req = HTTP.authed_req(user, :get, url)
-
- {:ok, response} = Finch.request(req, HTTP)
- json = Jason.decode!(response.body)
-
- registers =
- Enum.map(json, fn item ->
- {:ok, timestamp, _} = DateTime.from_iso8601(item["timeStamp"])
-
- %Device.Register{
- register_name: item["registerName"],
- register_value: item["registerValue"],
- timestamp: timestamp
- }
- end)
-
- %Device.RegisterCollection{
- outdoor_temp: find_register(registers, "REG_OUTDOOR_TEMPERATURE"),
- supply_out: find_register(registers, "REG_SUPPLY_LINE"),
- supply_in: find_register(registers, "REG_OPER_DATA_RETURN"),
- desired_supply: find_register(registers, "REG_DESIRED_SYS_SUPPLY_LINE_TEMP"),
- brine_out: find_register(registers, "REG_BRINE_OUT"),
- brine_in: find_register(registers, "REG_BRINE_IN"),
- hot_water_temp: find_register(registers, "REG_HOT_WATER_TEMPERATURE")
- }
- end
-
- @spec opstat(Auth.User.t(), Device.t()) :: Device.OpStat.t()
- def opstat(user, device) do
- url =
- Application.get_env(:geo_therminator, :api_device_opstat_url)
- |> String.replace("{id}", to_string(device.id))
-
- req = HTTP.authed_req(user, :get, url)
-
- {:ok, response} = Finch.request(req, HTTP)
- json = Jason.decode!(response.body)
-
- registers =
- Enum.map(json, fn item ->
- {:ok, timestamp, _} = DateTime.from_iso8601(item["timeStamp"])
-
- %Device.Register{
- register_name: item["registerName"],
- register_value: item["registerValue"],
- timestamp: timestamp
- }
- end)
-
- priority_register = find_register(registers, "REG_OPERATIONAL_STATUS_PRIO1")
-
- priority_register_fallback =
- find_register(registers, "REG_OPERATIONAL_STATUS_PRIORITY_BITMASK")
-
- {mapping, register} =
- if not is_nil(priority_register) do
- {Application.get_env(:geo_therminator, :api_opstat_mapping), priority_register}
- else
- {Application.get_env(:geo_therminator, :api_opstat_bitmask_mapping),
- priority_register_fallback}
- end
-
- %Device.OpStat{
- priority: Map.get(mapping, register.register_value, :unknown)
- }
- end
-
- @spec set_temp(Auth.User.t(), Device.t(), integer()) :: :ok | {:error, String.t()}
- def set_temp(user, device, temp) do
- url =
- Application.get_env(:geo_therminator, :api_device_reg_set_url)
- |> String.replace("{id}", to_string(device.id))
-
- register_index = Application.get_env(:geo_therminator, :api_device_temp_set_reg_index)
- client_id = Application.get_env(:geo_therminator, :api_device_reg_set_client_id)
-
- req =
- HTTP.authed_req(user, :post, url, [], %{
- registerIndex: register_index,
- registerValue: temp,
- clientUuid: client_id
- })
-
- IO.inspect(req)
- {:ok, response} = Finch.request(req, HTTP)
-
- if response.status == 200 do
- :ok
- else
- {:error, "Error #{response.status}: " <> response.body}
- end
- end
-
- defp find_register(registers, name) do
- Enum.find(registers, &(&1.register_name == name))
- end
-end
diff --git a/lib/geo_therminator/pump_api/device/device.ex b/lib/geo_therminator/pump_api/device/device.ex
index 9310a89..8dd8c1c 100644
--- a/lib/geo_therminator/pump_api/device/device.ex
+++ b/lib/geo_therminator/pump_api/device/device.ex
@@ -1,61 +1,50 @@
defmodule GeoTherminator.PumpAPI.Device do
- import GeoTherminator.TypedStruct
+ require Record
- deftypedstruct(%{
- id: integer(),
- device_id: integer(),
- is_online: boolean(),
- last_online: NaiveDateTime.t(),
- created_when: NaiveDateTime.t(),
- mac_address: String.t(),
- name: String.t(),
- model: String.t(),
- retailer_access: integer()
- })
+ Record.defrecord(:record, :device, [
+ :id,
+ :device_id,
+ :is_online,
+ :last_online,
+ :created_when,
+ :mac_address,
+ :name,
+ :model,
+ :retailer_access
+ ])
+
+ @type t :: :pump_api@device.device()
defmodule Status do
- deftypedstruct(%{
- heating_effect: integer(),
- is_heating_effect_set_by_user: boolean()
- })
+ Record.defrecord(:record, :status, [:heating_effect, :is_heating_effect_set_by_user])
+
+ @type t :: :pump_api@device.status()
end
defmodule Register do
- deftypedstruct(%{
- register_name: String.t(),
- register_value: integer(),
- timestamp: DateTime.t()
- })
+ Record.defrecord(:record, :register, [:name, :value, :timestamp])
+
+ @type t :: :pump_api@device.register()
end
defmodule RegisterCollection do
- deftypedstruct(%{
- outdoor_temp: Register.t(),
- supply_out: Register.t(),
- supply_in: Register.t(),
- desired_supply: Register.t(),
- brine_out: Register.t(),
- brine_in: Register.t(),
- hot_water_temp: Register.t()
- })
+ Record.defrecord(:record, :register_collection, [
+ :outdoor_temp,
+ :supply_out,
+ :supply_in,
+ :desired_supply,
+ :brine_out,
+ :brine_in,
+ :hot_water_temp
+ ])
+
+ @type t :: :pump_api@device.register_collection()
end
defmodule OpStat do
- deftypedstruct(%{
- priority:
- :hand_operated
- | :hot_water
- | :heating
- | :active_cooling
- | :pool
- | :anti_legionella
- | :passive_cooling
- | :standby
- | :idle
- | :off
- | :defrost
- | :unknown
- })
+ Record.defrecord(:record, :op_stat, [:priority])
+
+ @type t :: :pump_api@device.op_stat()
end
@spec get_device_process(GenServer.name(), GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) ::
diff --git a/lib/geo_therminator/pump_api/device/pub_sub.ex b/lib/geo_therminator/pump_api/device/pub_sub.ex
index 56a8d63..f788efa 100644
--- a/lib/geo_therminator/pump_api/device/pub_sub.ex
+++ b/lib/geo_therminator/pump_api/device/pub_sub.ex
@@ -1,8 +1,10 @@
defmodule GeoTherminator.PumpAPI.Device.PubSub do
require GeoTherminator.PumpAPI.Auth.InstallationInfo
+ require GeoTherminator.PumpAPI.Device
alias Phoenix.PubSub
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
+ alias GeoTherminator.PumpAPI.Device
@installation_topic "installation:"
@@ -20,7 +22,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
:ok =
PubSub.broadcast!(
__MODULE__,
- @installation_topic <> to_string(device.id),
+ @installation_topic <> to_string(Device.record(device, :id)),
{:device, device}
)
end
@@ -33,7 +35,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
:ok =
PubSub.broadcast!(
__MODULE__,
- @installation_topic <> to_string(device.id),
+ @installation_topic <> to_string(Device.record(device, :id)),
{:status, status}
)
end
@@ -46,7 +48,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
:ok =
PubSub.broadcast!(
__MODULE__,
- @installation_topic <> to_string(device.id),
+ @installation_topic <> to_string(Device.record(device, :id)),
{:registers, registers}
)
end
@@ -59,7 +61,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
:ok =
PubSub.broadcast!(
__MODULE__,
- @installation_topic <> to_string(device.id),
+ @installation_topic <> to_string(Device.record(device, :id)),
{:opstat, opstat}
)
end
diff --git a/lib/geo_therminator/pump_api/device/server.ex b/lib/geo_therminator/pump_api/device/server.ex
index c0466a4..bd98fb7 100644
--- a/lib/geo_therminator/pump_api/device/server.ex
+++ b/lib/geo_therminator/pump_api/device/server.ex
@@ -40,8 +40,7 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
@impl true
def handle_continue({:init, installation}, state) do
user = AuthServer.get_auth(state.auth_server)
- device = Device.API.device_info(user, installation)
-
+ {:ok, device} = :pump_api@device@api.device_info(user, installation)
:ok = Device.PubSub.broadcast_device(device)
state =
@@ -75,7 +74,7 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
user = AuthServer.get_auth(state.auth_server)
Logger.debug("Begin set temp to #{temp}")
- resp = Device.API.set_temp(user, state.device, temp)
+ resp = :pump_api@device@api.set_temp(user, state.device, temp)
Logger.debug("Set temp result: #{inspect(resp)}")
{:reply, resp, state}
end
@@ -119,11 +118,15 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
[status, registers, opstat] =
Task.async_stream(
- [&Device.API.status/2, &Device.API.register_info/2, &Device.API.opstat/2],
+ [
+ &:pump_api@device@api.status/2,
+ &:pump_api@device@api.register_info/2,
+ &:pump_api@device@api.opstat/2
+ ],
& &1.(user, state.device),
timeout: Application.fetch_env!(:geo_therminator, :api_timeout)
)
- |> Enum.map(fn {:ok, val} -> val end)
+ |> Enum.map(fn {:ok, {:ok, val}} -> val end)
Device.PubSub.broadcast_status(state.device, status)
Device.PubSub.broadcast_registers(state.device, registers)
diff --git a/lib/geo_therminator_web/live/components/main_view.ex b/lib/geo_therminator_web/live/components/main_view.ex
index 474d246..4547638 100644
--- a/lib/geo_therminator_web/live/components/main_view.ex
+++ b/lib/geo_therminator_web/live/components/main_view.ex
@@ -1,19 +1,51 @@
defmodule GeoTherminatorWeb.Components.MainView do
+ require GeoTherminator.PumpAPI.Device
+ require GeoTherminator.PumpAPI.Device.Status
+ require GeoTherminator.PumpAPI.Device.RegisterCollection
+ require GeoTherminator.PumpAPI.Device.OpStat
+ require GeoTherminator.PumpAPI.Device.Register
+
use GeoTherminatorWeb, :live_component
+ alias GeoTherminator.PumpAPI.Device
+
@impl true
def update(assigns, socket) do
{:ok,
assign(socket,
- set_temp: assigns.status.heating_effect,
- set_temp_active: assigns.status.is_heating_effect_set_by_user,
- hot_water_temp: assigns.registers.hot_water_temp.register_value,
- brine_out: assigns.registers.brine_out.register_value,
- brine_in: assigns.registers.brine_in.register_value,
- supply_out: assigns.registers.supply_out.register_value,
- supply_in: assigns.registers.supply_in.register_value,
- outdoor_temp: assigns.registers.outdoor_temp.register_value,
- priority: assigns.opstat.priority
+ set_temp: Device.Status.record(assigns.status, :heating_effect),
+ set_temp_active: Device.Status.record(assigns.status, :is_heating_effect_set_by_user),
+ hot_water_temp:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :hot_water_temp),
+ :value
+ ),
+ brine_out:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :brine_out),
+ :value
+ ),
+ brine_in:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :brine_in),
+ :value
+ ),
+ supply_out:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :supply_out),
+ :value
+ ),
+ supply_in:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :supply_in),
+ :value
+ ),
+ outdoor_temp:
+ Device.Register.record(
+ Device.RegisterCollection.record(assigns.registers, :outdoor_temp),
+ :value
+ ),
+ priority: Device.OpStat.record(assigns.opstat, :priority)
)}
end
end
diff --git a/lib/geo_therminator_web/live/main/device_list.ex b/lib/geo_therminator_web/live/main/device_list.ex
index ee20248..2b1919c 100644
--- a/lib/geo_therminator_web/live/main/device_list.ex
+++ b/lib/geo_therminator_web/live/main/device_list.ex
@@ -1,5 +1,7 @@
defmodule GeoTherminatorWeb.MainLive.DeviceList do
+ require GeoTherminator.PumpAPI.Auth.InstallationInfo
use GeoTherminatorWeb, :live_view
+ alias GeoTherminator.PumpAPI.Auth.InstallationInfo
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
diff --git a/lib/geo_therminator_web/live/main/device_list.html.heex b/lib/geo_therminator_web/live/main/device_list.html.heex
index 51cba2a..22ab26a 100644
--- a/lib/geo_therminator_web/live/main/device_list.html.heex
+++ b/lib/geo_therminator_web/live/main/device_list.html.heex
@@ -8,8 +8,14 @@
<%= for installation <- @installations do %>
-
- <%= live_redirect(installation.id,
- to: Routes.live_path(@socket, GeoTherminatorWeb.MainLive.Pump, installation.id)
+ <%= live_redirect(
+ InstallationInfo.record(installation, :id),
+ to:
+ Routes.live_path(
+ @socket,
+ GeoTherminatorWeb.MainLive.Pump,
+ InstallationInfo.record(installation, :id)
+ )
) %>
<% end %>
diff --git a/lib/geo_therminator_web/live/main/index.ex b/lib/geo_therminator_web/live/main/index.ex
index 66a35b2..81ce595 100644
--- a/lib/geo_therminator_web/live/main/index.ex
+++ b/lib/geo_therminator_web/live/main/index.ex
@@ -58,7 +58,7 @@ defmodule GeoTherminatorWeb.MainLive.Index do
err ->
Logger.debug("Error starting auth server: #{inspect(err)}")
- assign(socket, error: true)
+ assign(socket, error: err)
end
end
end
diff --git a/lib/geo_therminator_web/live/main/index.html.heex b/lib/geo_therminator_web/live/main/index.html.heex
index 347d5b8..4f72ca1 100644
--- a/lib/geo_therminator_web/live/main/index.html.heex
+++ b/lib/geo_therminator_web/live/main/index.html.heex
@@ -7,8 +7,8 @@
- <%= if @error do %>
- Error occurred, please try again.
+ <%= if @error != false do %>
+ Error occurred, please try again: <%= inspect(@error) %>
<% end %>
<% else %>
diff --git a/lib/geo_therminator_web/live/main/pump.ex b/lib/geo_therminator_web/live/main/pump.ex
index d6e7cad..d92ab78 100644
--- a/lib/geo_therminator_web/live/main/pump.ex
+++ b/lib/geo_therminator_web/live/main/pump.ex
@@ -1,4 +1,9 @@
defmodule GeoTherminatorWeb.MainLive.Pump do
+ require GeoTherminator.PumpAPI.Device
+ require GeoTherminator.PumpAPI.Device.Status
+ require GeoTherminator.PumpAPI.Device.RegisterCollection
+ require GeoTherminator.PumpAPI.Device.OpStat
+ require GeoTherminator.PumpAPI.Device.Register
use GeoTherminatorWeb, :live_view
alias GeoTherminator.PumpAPI.Auth
alias GeoTherminator.PumpAPI.Device
@@ -11,10 +16,10 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
info when info != nil <- Auth.Server.get_installation(Auth.Server, id),
:ok <- Device.PubSub.subscribe_installation(info),
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
- %Device{} = device <- Device.Server.get_device(pid),
- %Device.Status{} = status <- Device.Server.get_status(pid),
- %Device.RegisterCollection{} = registers <- Device.Server.get_registers(pid),
- %Device.OpStat{} = opstat <- Device.Server.get_opstat(pid) do
+ device <- Device.Server.get_device(pid),
+ status <- Device.Server.get_status(pid),
+ registers <- Device.Server.get_registers(pid),
+ opstat <- Device.Server.get_opstat(pid) do
CubDB.put(GeoTherminator.DB, :viewing_pump, str_id)
assign(socket,
@@ -37,14 +42,15 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
def handle_event(event, unsigned_params, socket)
def handle_event("inc_temp", _params, socket) do
- if not socket.assigns.status.is_heating_effect_set_by_user do
- current = socket.assigns.status.heating_effect
+ if not Device.Status.record(socket.assigns.status, :is_heating_effect_set_by_user) do
+ current = Device.Status.record(socket.assigns.status, :heating_effect)
_ = Device.Server.set_temp(socket.assigns.pid, current + 1)
- optimistic_status = %Device.Status{
- socket.assigns.status
- | is_heating_effect_set_by_user: true
- }
+ optimistic_status =
+ Device.Status.record(
+ socket.assigns.status,
+ is_heating_effect_set_by_user: true
+ )
{:noreply, assign(socket, status: optimistic_status)}
else
@@ -53,14 +59,15 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
end
def handle_event("dec_temp", _params, socket) do
- if not socket.assigns.status.is_heating_effect_set_by_user do
- current = socket.assigns.status.heating_effect
+ if not Device.Status.record(socket.assigns.status, :is_heating_effect_set_by_user) do
+ current = Device.Status.record(socket.assigns.status, :heating_effect)
_ = Device.Server.set_temp(socket.assigns.pid, current - 1)
- optimistic_status = %Device.Status{
- socket.assigns.status
- | is_heating_effect_set_by_user: true
- }
+ optimistic_status =
+ Device.Status.record(
+ socket.assigns.status,
+ is_heating_effect_set_by_user: true
+ )
{:noreply, assign(socket, status: optimistic_status)}
else
diff --git a/lib/geo_therminator_web/live/main/pump.html.heex b/lib/geo_therminator_web/live/main/pump.html.heex
index 6a3c995..5f8a7dc 100644
--- a/lib/geo_therminator_web/live/main/pump.html.heex
+++ b/lib/geo_therminator_web/live/main/pump.html.heex
@@ -6,7 +6,7 @@
<% else %>
<.live_component
module={GeoTherminatorWeb.Components.MainView}
- id={"pump-#{@device.id}"}
+ id={"pump-#{Device.record(@device, :id)}"}
device={@device}
status={@status}
registers={@registers}
diff --git a/mix.exs b/mix.exs
index 3168007..e94467f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -76,7 +76,7 @@ defmodule GeoTherminator.MixProject do
{:finch, "~> 0.9.0"},
{:desktop, "~> 1.4"},
{:cubdb, "~> 2.0"},
- {:gleam_stdlib, "~> 0.25"},
+ {:gleam_stdlib, "~> 0.26"},
{:gleam_http, "~> 3.1"},
{:gleam_erlang, "~> 0.17.1"},
{:gleam_json, "~> 0.5.0"}
diff --git a/mix.lock b/mix.lock
index 14b71e6..c61e10e 100644
--- a/mix.lock
+++ b/mix.lock
@@ -20,7 +20,7 @@
"gleam_erlang": {:hex, :gleam_erlang, "0.17.1", "40fff501e8ca39fa166f4c12ed13bb57e94fc5bb59a93b4446687d82d4a12ff9", [:gleam], [{:gleam_stdlib, "~> 0.22", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}], "hexpm", "baaa84f5bcc4477e809ba3e03bb3009a3894a6544c1511626c44408e39db2ae6"},
"gleam_http": {:hex, :gleam_http, "3.1.1", "609158240630e21fc70c69b21384e5ebbcd86f71bd378a6f7c2b87f910ab3561", [:gleam], [{:gleam_stdlib, "~> 0.18", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}], "hexpm", "b66b7a1539ccb577119e4dc80dd3484c1a652cb032967954498eedbae3355763"},
"gleam_json": {:hex, :gleam_json, "0.5.0", "aff4507ad7700ad794ada6671c6dfd0174696713659bd8782858135b19f41b58", [:gleam], [{:gleam_stdlib, "~> 0.19", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}, {:thoas, "~> 0.2", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "e42443c98aa66e30143c24818f2cea801491c10ce6b1a5eddf3fc4abdc7601cb"},
- "gleam_stdlib": {:hex, :gleam_stdlib, "0.25.0", "656f39258dcc8772719e463bbe7d1d1c7800238a520b41558fad53ea206ee3ab", [:gleam], [], "hexpm", "ad0f89928e0b919c8f8edf640484633b28dbf88630a9e6ae504617a3e3e5b9a2"},
+ "gleam_stdlib": {:hex, :gleam_stdlib, "0.26.0", "2f5814a495eeabfd0c63fe22bf26d054c544e641f3cfcf114ee74e91548272f7", [:gleam], [], "hexpm", "6221f9d7a08b6d6dbcdd567b2bb7c4b2a7bbf4c04c6110757be04635143bdec8"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
diff --git a/src/azure/b2c.gleam b/src/azure/b2c.gleam
index 0c8d858..c32f859 100644
--- a/src/azure/b2c.gleam
+++ b/src/azure/b2c.gleam
@@ -2,6 +2,9 @@
//// Mostly translated from:
//// https://github.com/klejejs/python-thermia-online-api/blob/2f0ec4e45bfecbd90932a10247283cbcd6a6c48c/ThermiaOnlineAPI/api/ThermiaAPI.py
//// Used under the Gnu General Public License 3.0
+////
+//// Refreshing using the refresh token is not implemented, we just get a new
+//// access token every time, YOLO
import gleam/json
import gleam/base
@@ -23,14 +26,6 @@ import helpers/parsing
const challenge_length = 43
-const b2c_client_id = "09ea4903-9e95-45fe-ae1f-e3b7d32fa385"
-
-const b2c_scope = b2c_client_id
-
-const b2c_redirect_uri = "https://online-genesis.thermia.se/login"
-
-const b2c_auth_url = "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft.com/b2c_1a_signuporsigninonline"
-
const b2c_authorize_prefix = "var SETTINGS = "
pub type Tokens {
@@ -74,10 +69,6 @@ pub fn authenticate(
get_tokens(confirmed, code_challenge)
}
-pub fn refresh(tokens: Tokens) -> Result(Tokens, B2CError) {
- todo
-}
-
fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
let auth_data = [
#("response_type", "code"),
@@ -250,25 +241,28 @@ fn hash_challenge(challenge: String) -> String {
base.url_encode64(hashed, False)
}
-fn authorize_url() -> String {
- b2c_auth_url <> "/oauth2/v2.0/authorize"
+fn authorize_url() -> uri.Uri {
+ let url = config.api_url("b2c_auth_url")
+ uri.Uri(..url, path: url.path <> "/oauth2/v2.0/authorize")
}
-fn self_asserted_url() -> String {
- b2c_auth_url <> "/SelfAsserted"
+fn self_asserted_url() -> uri.Uri {
+ let url = config.api_url("b2c_auth_url")
+ uri.Uri(..url, path: url.path <> "/SelfAsserted")
}
-fn confirm_url() -> String {
- b2c_auth_url <> "/api/CombinedSigninAndSignup/confirmed"
+fn confirm_url() -> uri.Uri {
+ let url = config.api_url("b2c_auth_url")
+ uri.Uri(..url, path: url.path <> "/api/CombinedSigninAndSignup/confirmed")
}
-fn get_token_url() -> String {
- b2c_auth_url <> "/oauth2/v2.0/token"
+fn get_token_url() -> uri.Uri {
+ let url = config.api_url("b2c_auth_url")
+ uri.Uri(..url, path: url.path <> "/oauth2/v2.0/token")
}
-fn build_req(url: String, method: http.Method) -> request.Request(String) {
- assert Ok(req_url) = uri.parse(url)
- assert Ok(req) = request.from_uri(req_url)
+fn build_req(url: uri.Uri, method: http.Method) -> request.Request(String) {
+ assert Ok(req) = request.from_uri(url)
let req = request.set_method(req, method)
@@ -304,10 +298,12 @@ fn run_req(
}
fn base_request_data() -> List(#(String, String)) {
+ let b2c_client_id = config.str("b2c_client_id")
+
[
#("client_id", b2c_client_id),
- #("scope", b2c_scope),
- #("redirect_uri", b2c_redirect_uri),
+ #("scope", b2c_client_id),
+ #("redirect_uri", config.str("b2c_redirect_url")),
]
}
diff --git a/src/helpers/config.gleam b/src/helpers/config.gleam
index ff7baf0..248e8f2 100644
--- a/src/helpers/config.gleam
+++ b/src/helpers/config.gleam
@@ -18,24 +18,35 @@ pub fn api_timeout() -> Int {
timeout_int
}
-pub fn api_auth_url() -> uri.Uri {
- config_url("api_auth_url")
+pub fn api_url(config_key: String) -> uri.Uri {
+ map_api_url(config_key, fn(s) { s })
}
-pub fn api_installations_url() -> uri.Uri {
- config_url("api_installations_url")
+pub fn map_api_url(config_key: String, mapper: fn(String) -> String) -> uri.Uri {
+ let url =
+ application.fetch_env_angry(app_name(), atom.create_from_string(config_key))
+
+ assert Ok(url_str) = dynamic.string(url)
+ let url_str = mapper(url_str)
+ assert Ok(parsed_url) = uri.parse(url_str)
+ parsed_url
+}
+
+pub fn str(config_key: String) -> String {
+ let val =
+ application.fetch_env_angry(app_name(), atom.create_from_string(config_key))
+ assert Ok(val_str) = dynamic.string(val)
+ val_str
+}
+
+pub fn int(config_key: String) -> Int {
+ let val =
+ application.fetch_env_angry(app_name(), atom.create_from_string(config_key))
+ assert Ok(val_int) = dynamic.int(val)
+ val_int
}
fn app_name() -> atom.Atom {
assert Ok(name) = atom.from_string("geo_therminator")
name
}
-
-fn config_url(config_key: String) -> uri.Uri {
- let url =
- application.fetch_env_angry(app_name(), atom.create_from_string(config_key))
-
- assert Ok(url_str) = dynamic.string(url)
- assert Ok(parsed_url) = uri.parse(url_str)
- parsed_url
-}
diff --git a/src/helpers/date_time.gleam b/src/helpers/date_time.gleam
index 973f041..844ad9a 100644
--- a/src/helpers/date_time.gleam
+++ b/src/helpers/date_time.gleam
@@ -1,8 +1,38 @@
+import gleam
import gleam/erlang/atom.{Atom}
+import gleam/dynamic
+
+pub type FromDynamicError {
+ NotAString
+ ParseError
+}
pub external type DateTime
-pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) =
+pub type DateTimeFromISO8601Result {
+ Ok(datetime: DateTime, offset: Int)
+ Error(reason: Atom)
+}
+
+pub fn from_dynamic_iso8601(
+ data: dynamic.Dynamic,
+) -> Result(#(DateTime, Int), List(dynamic.DecodeError)) {
+ try data_str = dynamic.string(data)
+
+ case from_iso8601(data_str) {
+ Ok(datetime, offset) -> gleam.Ok(#(datetime, offset))
+ Error(_reason) ->
+ gleam.Error([
+ dynamic.DecodeError(
+ expected: "ISO-8601 formatted datetime with timezone",
+ found: data_str,
+ path: [],
+ ),
+ ])
+ }
+}
+
+pub external fn from_iso8601(String) -> DateTimeFromISO8601Result =
"Elixir.DateTime" "from_iso8601"
pub external fn from_unix(Int) -> Result(DateTime, #(Atom, Atom)) =
diff --git a/src/helpers/naive_date_time.gleam b/src/helpers/naive_date_time.gleam
new file mode 100644
index 0000000..4d00229
--- /dev/null
+++ b/src/helpers/naive_date_time.gleam
@@ -0,0 +1,22 @@
+import gleam/dynamic
+import gleam/erlang/atom.{Atom}
+import gleam/result
+
+pub external type NaiveDateTime
+
+pub fn from_dynamic_iso8601(
+ data: dynamic.Dynamic,
+) -> Result(NaiveDateTime, List(dynamic.DecodeError)) {
+ try data_str = dynamic.string(data)
+ from_iso8601(data_str)
+ |> result.replace_error([
+ dynamic.DecodeError(
+ expected: "ISO-8601 formatted naive datetime",
+ found: data_str,
+ path: [],
+ ),
+ ])
+}
+
+pub external fn from_iso8601(String) -> Result(NaiveDateTime, Atom) =
+ "Elixir.NaiveDateTime" "from_iso8601"
diff --git a/src/pump_api/api.gleam b/src/pump_api/api.gleam
new file mode 100644
index 0000000..aca00b8
--- /dev/null
+++ b/src/pump_api/api.gleam
@@ -0,0 +1,17 @@
+import azure/b2c.{B2CError}
+import pump_api/http
+
+pub type ApiError {
+ RequestFailed
+ NotOkResponse
+ InvalidData(msg: String)
+ AuthError(inner: B2CError)
+}
+
+pub fn from_http(err: http.ApiError) -> ApiError {
+ case err {
+ http.RequestFailed -> RequestFailed
+ http.NotOkResponse -> NotOkResponse
+ http.InvalidData -> InvalidData("Invalid data in HTTP response.")
+ }
+}
diff --git a/src/pump_api/auth/api.gleam b/src/pump_api/auth/api.gleam
index 59de141..88652c8 100644
--- a/src/pump_api/auth/api.gleam
+++ b/src/pump_api/auth/api.gleam
@@ -1,5 +1,4 @@
import gleam/http/request
-import gleam/json
import gleam/result
import gleam/dynamic
import gleam/list
@@ -8,18 +7,11 @@ import pump_api/auth/user.{User}
import pump_api/auth/tokens.{Tokens}
import pump_api/auth/installation_info.{InstallationInfo}
import pump_api/http
+import pump_api/api.{ApiError, AuthError, InvalidData}
import helpers/config
import helpers/date_time
import helpers/parsing
-import helpers/finch
-import azure/b2c.{B2CError}
-
-pub type ApiError {
- ApiRequestFailed
- NotOkResponse
- InvalidData(msg: String)
- AuthError(inner: B2CError)
-}
+import azure/b2c
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
try tokens =
@@ -49,11 +41,15 @@ pub fn auth(username: String, password: String) -> Result(User, ApiError) {
}
pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError) {
- let url = config.api_installations_url()
+ let url = config.api_url("api_installations_url")
assert Ok(raw_req) = request.from_uri(url)
let empty_req = request.set_body(raw_req, http.Empty)
- try data = run_req(http.authed_req(user, empty_req))
+ try data =
+ http.run_json_req(http.authed_req(user, empty_req))
+ |> result.replace_error(InvalidData(
+ msg: "Could not parse InstallationInfo JSON.",
+ ))
try items =
parsing.data_get(
@@ -65,22 +61,3 @@ pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError)
Ok(list.map(items, fn(id) { InstallationInfo(id: id) }))
}
-
-fn run_req(req: request.Request(String)) {
- try resp =
- req
- |> finch.build()
- |> finch.request(config.finch_server())
- |> result.replace_error(ApiRequestFailed)
-
- try body = case resp.status {
- 200 -> Ok(resp.body)
- _ -> Error(NotOkResponse)
- }
-
- body
- |> json.decode(using: dynamic.dynamic)
- |> result.replace_error(InvalidData(
- msg: "Could not parse InstallationInfo JSON.",
- ))
-}
diff --git a/src/pump_api/device.gleam b/src/pump_api/device.gleam
new file mode 100644
index 0000000..a6a644e
--- /dev/null
+++ b/src/pump_api/device.gleam
@@ -0,0 +1,56 @@
+import gleam/set
+import helpers/naive_date_time.{NaiveDateTime}
+import helpers/date_time.{DateTime}
+
+pub type Device {
+ Device(
+ id: Int,
+ device_id: Int,
+ is_online: Bool,
+ last_online: NaiveDateTime,
+ created_when: NaiveDateTime,
+ mac_address: String,
+ name: String,
+ model: String,
+ retailer_access: Int,
+ )
+}
+
+pub type Status {
+ Status(heating_effect: Int, is_heating_effect_set_by_user: Bool)
+}
+
+pub type Register {
+ Register(name: String, value: Int, timestamp: DateTime)
+}
+
+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,
+ )
+}
+
+pub type Priority {
+ HandOperated
+ HotWater
+ Heating
+ ActiveCooling
+ Pool
+ AntiLegionella
+ PassiveCooling
+ Standby
+ Idle
+ Off
+ Defrost
+ Unknown
+}
+
+pub type OpStat {
+ OpStat(priority: set.Set(Priority))
+}
diff --git a/src/pump_api/device/api.gleam b/src/pump_api/device/api.gleam
new file mode 100644
index 0000000..bf791b0
--- /dev/null
+++ b/src/pump_api/device/api.gleam
@@ -0,0 +1,286 @@
+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 pump_api/auth/user.{User}
+import pump_api/auth/installation_info.{InstallationInfo}
+import pump_api/device.{
+ Device, OpStat, Priority, Register, RegisterCollection, Status,
+}
+import pump_api/http
+import pump_api/api.{ApiError, InvalidData}
+import helpers/config
+import helpers/parsing
+import helpers/naive_date_time
+import helpers/date_time
+
+const opstat_bitmask_mapping = [
+ #(1, device.HandOperated),
+ #(2, device.Defrost),
+ #(4, device.HotWater),
+ #(8, device.Heating),
+ #(16, device.ActiveCooling),
+ #(32, device.Pool),
+ #(64, device.AntiLegionella),
+ #(128, device.PassiveCooling),
+ #(512, device.Standby),
+ #(1024, device.Idle),
+ #(2048, device.Off),
+]
+
+pub fn device_info(
+ user: User,
+ installation: InstallationInfo,
+) -> Result(Device, ApiError) {
+ let url =
+ config.map_api_url(
+ "api_device_url",
+ fn(url) { string.replace(url, "{id}", int.to_string(installation.id)) },
+ )
+ assert Ok(raw_req) = request.from_uri(url)
+
+ let empty_req = request.set_body(raw_req, http.Empty)
+ try data =
+ http.run_json_req(http.authed_req(user, empty_req))
+ |> result.map_error(api.from_http)
+
+ try last_online =
+ parsing.data_get(
+ data,
+ "lastOnline",
+ naive_date_time.from_dynamic_iso8601,
+ InvalidData,
+ )
+ try created_when =
+ parsing.data_get(
+ data,
+ "createdWhen",
+ naive_date_time.from_dynamic_iso8601,
+ InvalidData,
+ )
+ try id = parsing.data_get(data, "id", dynamic.int, InvalidData)
+ try device_id = parsing.data_get(data, "deviceId", dynamic.int, InvalidData)
+ try is_online = parsing.data_get(data, "isOnline", dynamic.bool, InvalidData)
+ try mac_address =
+ parsing.data_get(data, "macAddress", dynamic.string, InvalidData)
+ try name = parsing.data_get(data, "name", dynamic.string, InvalidData)
+ try model = parsing.data_get(data, "model", dynamic.string, InvalidData)
+ try retailer_access =
+ 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(user: User, device: Device) {
+ let url =
+ config.map_api_url(
+ "api_device_status_url",
+ fn(url) { string.replace(url, "{id}", int.to_string(device.id)) },
+ )
+ assert Ok(raw_req) = request.from_uri(url)
+
+ let empty_req = request.set_body(raw_req, http.Empty)
+ try data =
+ http.run_json_req(http.authed_req(user, empty_req))
+ |> result.map_error(api.from_http)
+
+ try heating_effect =
+ parsing.data_get(data, "heatingEffect", dynamic.int, InvalidData)
+ try is_heating_effect_set_by_user =
+ 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(user: User, device: Device) {
+ let url =
+ config.map_api_url(
+ "api_device_register_url",
+ fn(url) { string.replace(url, "{id}", int.to_string(device.id)) },
+ )
+ assert Ok(raw_req) = request.from_uri(url)
+
+ let empty_req = request.set_body(raw_req, http.Empty)
+ try data =
+ http.run_json_req(http.authed_req(user, empty_req))
+ |> result.map_error(api.from_http)
+
+ try registers =
+ 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()
+
+ try outdoor_temp = get_register(registers_map, "REG_OUTDOOR_TEMPERATURE")
+ try supply_out = get_register(registers_map, "REG_SUPPLY_LINE")
+ try supply_in = get_register(registers_map, "REG_OPER_DATA_RETURN")
+ try desired_supply =
+ get_register(registers_map, "REG_DESIRED_SYS_SUPPLY_LINE_TEMP")
+ try brine_out = get_register(registers_map, "REG_BRINE_OUT")
+ try brine_in = get_register(registers_map, "REG_BRINE_IN")
+ try hot_water_temp = 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(user: User, device: Device) {
+ let url =
+ config.map_api_url(
+ "api_device_opstat_url",
+ fn(url) { string.replace(url, "{id}", int.to_string(device.id)) },
+ )
+ assert Ok(raw_req) = request.from_uri(url)
+
+ let empty_req = request.set_body(raw_req, http.Empty)
+ try data =
+ http.run_json_req(http.authed_req(user, empty_req))
+ |> result.map_error(api.from_http)
+
+ try registers =
+ 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")
+
+ try priority = case #(priority_register, priority_register_fallback) {
+ #(Ok(data), _) -> Ok(opstat_map(data))
+ #(_, Ok(data)) -> Ok(opstat_bitmask_map(data))
+ _ ->
+ Error(InvalidData(
+ "Unable to parse opstat: " <> string.inspect(#(
+ priority_register,
+ priority_register_fallback,
+ )),
+ ))
+ }
+
+ Ok(OpStat(priority: priority))
+}
+
+pub fn set_temp(user: User, device: Device, temp: Int) {
+ let url =
+ config.map_api_url(
+ "api_device_opstat_url",
+ fn(url) { string.replace(url, "{id}", int.to_string(device.id)) },
+ )
+ assert Ok(raw_req) = request.from_uri(url)
+
+ let register_index = config.int("api_device_temp_set_reg_index")
+ let client_id = config.str("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)) {
+ try #(timestamp, _) =
+ dynamic.field("timeStamp", date_time.from_dynamic_iso8601)(item)
+ try name = dynamic.field("registerName", dynamic.string)(item)
+ try value = 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 -> device.HandOperated
+ 3 -> device.HotWater
+ 4 -> device.Heating
+ 5 -> device.ActiveCooling
+ 6 -> device.Pool
+ 7 -> device.AntiLegionella
+ 8 -> device.PassiveCooling
+ 98 -> device.Standby
+ 99 -> device.Idle
+ 100 -> device.Off
+ _ -> device.Unknown
+ }
+
+ set.new()
+ |> set.insert(val)
+}
+
+fn opstat_bitmask_map(register: Register) {
+ let priority_set: set.Set(Priority) = set.new()
+
+ list.fold(
+ opstat_bitmask_mapping,
+ priority_set,
+ fn(output, mapping) {
+ let #(int, priority) = mapping
+
+ case band(register.value, int) {
+ 0 -> set.insert(output, priority)
+ _ -> priority_set
+ }
+ },
+ )
+}
+
+external fn band(i1: Int, i2: Int) -> Int =
+ "erlang" "band"
diff --git a/src/pump_api/http.gleam b/src/pump_api/http.gleam
index 6cc2f0f..4a44967 100644
--- a/src/pump_api/http.gleam
+++ b/src/pump_api/http.gleam
@@ -1,6 +1,10 @@
import gleam/http/request
import gleam/json.{Json}
+import gleam/result
+import gleam/dynamic
import pump_api/auth/user.{User}
+import helpers/finch
+import helpers/config
pub type Body {
Empty
@@ -11,6 +15,12 @@ pub type Body {
pub type ApiRequest =
request.Request(Body)
+pub type ApiError {
+ RequestFailed
+ NotOkResponse
+ InvalidData
+}
+
pub fn authed_req(user: User, r: ApiRequest) {
r
|> request.set_header("authorization", "Bearer " <> user.tokens.access_token)
@@ -29,3 +39,24 @@ pub fn req(r: ApiRequest) -> request.Request(String) {
|> request.set_body(json.to_string(data))
}
}
+
+pub fn run_req(req: request.Request(String)) {
+ try resp =
+ req
+ |> finch.build()
+ |> finch.request(config.finch_server())
+ |> result.replace_error(RequestFailed)
+
+ case resp.status {
+ 200 -> Ok(resp.body)
+ _ -> Error(NotOkResponse)
+ }
+}
+
+pub fn run_json_req(req: request.Request(String)) {
+ try body = run_req(req)
+
+ body
+ |> json.decode(using: dynamic.dynamic)
+ |> result.replace_error(InvalidData)
+}