Reimplement the rest of the pump API core code in Gleam
This commit is contained in:
parent
bcba4a92b3
commit
d7a05f3b1f
25 changed files with 633 additions and 319 deletions
|
@ -77,6 +77,13 @@ config :geo_therminator,
|
||||||
api_device_temp_set_reg_index: 3,
|
api_device_temp_set_reg_index: 3,
|
||||||
api_device_reg_set_client_id: get_env("API_DEVICE_REG_SET_CLIENT_ID"),
|
api_device_reg_set_client_id: get_env("API_DEVICE_REG_SET_CLIENT_ID"),
|
||||||
api_refresh: 10_000,
|
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
|
# Database directory for settings
|
||||||
db_dir:
|
db_dir:
|
||||||
|
|
|
@ -8,11 +8,5 @@ defmodule GeoTherminator.PumpAPI.Auth.Tokens do
|
||||||
:refresh_token_expiry
|
:refresh_token_expiry
|
||||||
])
|
])
|
||||||
|
|
||||||
@type t ::
|
@type t :: :pump_api@auth@tokens.tokens()
|
||||||
record(:record,
|
|
||||||
access_token: String.t(),
|
|
||||||
access_token_expiry: DateTime.t(),
|
|
||||||
refresh_token: String.t(),
|
|
||||||
refresh_token_expiry: DateTime.t()
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,5 +3,5 @@ defmodule GeoTherminator.PumpAPI.Auth.User do
|
||||||
|
|
||||||
Record.defrecord(:record, :user, [:tokens])
|
Record.defrecord(:record, :user, [:tokens])
|
||||||
|
|
||||||
@type t :: record(:record, tokens: GeoTherminator.PumpAPI.Auth.Tokens.t())
|
@type t :: :pump_api@auth@user.user()
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
|
@ -1,61 +1,50 @@
|
||||||
defmodule GeoTherminator.PumpAPI.Device do
|
defmodule GeoTherminator.PumpAPI.Device do
|
||||||
import GeoTherminator.TypedStruct
|
require Record
|
||||||
|
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :device, [
|
||||||
id: integer(),
|
:id,
|
||||||
device_id: integer(),
|
:device_id,
|
||||||
is_online: boolean(),
|
:is_online,
|
||||||
last_online: NaiveDateTime.t(),
|
:last_online,
|
||||||
created_when: NaiveDateTime.t(),
|
:created_when,
|
||||||
mac_address: String.t(),
|
:mac_address,
|
||||||
name: String.t(),
|
:name,
|
||||||
model: String.t(),
|
:model,
|
||||||
retailer_access: integer()
|
:retailer_access
|
||||||
})
|
])
|
||||||
|
|
||||||
|
@type t :: :pump_api@device.device()
|
||||||
|
|
||||||
defmodule Status do
|
defmodule Status do
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :status, [:heating_effect, :is_heating_effect_set_by_user])
|
||||||
heating_effect: integer(),
|
|
||||||
is_heating_effect_set_by_user: boolean()
|
@type t :: :pump_api@device.status()
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Register do
|
defmodule Register do
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :register, [:name, :value, :timestamp])
|
||||||
register_name: String.t(),
|
|
||||||
register_value: integer(),
|
@type t :: :pump_api@device.register()
|
||||||
timestamp: DateTime.t()
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule RegisterCollection do
|
defmodule RegisterCollection do
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :register_collection, [
|
||||||
outdoor_temp: Register.t(),
|
:outdoor_temp,
|
||||||
supply_out: Register.t(),
|
:supply_out,
|
||||||
supply_in: Register.t(),
|
:supply_in,
|
||||||
desired_supply: Register.t(),
|
:desired_supply,
|
||||||
brine_out: Register.t(),
|
:brine_out,
|
||||||
brine_in: Register.t(),
|
:brine_in,
|
||||||
hot_water_temp: Register.t()
|
:hot_water_temp
|
||||||
})
|
])
|
||||||
|
|
||||||
|
@type t :: :pump_api@device.register_collection()
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule OpStat do
|
defmodule OpStat do
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :op_stat, [:priority])
|
||||||
priority:
|
|
||||||
:hand_operated
|
@type t :: :pump_api@device.op_stat()
|
||||||
| :hot_water
|
|
||||||
| :heating
|
|
||||||
| :active_cooling
|
|
||||||
| :pool
|
|
||||||
| :anti_legionella
|
|
||||||
| :passive_cooling
|
|
||||||
| :standby
|
|
||||||
| :idle
|
|
||||||
| :off
|
|
||||||
| :defrost
|
|
||||||
| :unknown
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_device_process(GenServer.name(), GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) ::
|
@spec get_device_process(GenServer.name(), GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) ::
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
require GeoTherminator.PumpAPI.Device
|
||||||
|
|
||||||
alias Phoenix.PubSub
|
alias Phoenix.PubSub
|
||||||
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
|
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
alias GeoTherminator.PumpAPI.Device
|
||||||
|
|
||||||
@installation_topic "installation:"
|
@installation_topic "installation:"
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
:ok =
|
:ok =
|
||||||
PubSub.broadcast!(
|
PubSub.broadcast!(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
@installation_topic <> to_string(device.id),
|
@installation_topic <> to_string(Device.record(device, :id)),
|
||||||
{:device, device}
|
{:device, device}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -33,7 +35,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
:ok =
|
:ok =
|
||||||
PubSub.broadcast!(
|
PubSub.broadcast!(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
@installation_topic <> to_string(device.id),
|
@installation_topic <> to_string(Device.record(device, :id)),
|
||||||
{:status, status}
|
{:status, status}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -46,7 +48,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
:ok =
|
:ok =
|
||||||
PubSub.broadcast!(
|
PubSub.broadcast!(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
@installation_topic <> to_string(device.id),
|
@installation_topic <> to_string(Device.record(device, :id)),
|
||||||
{:registers, registers}
|
{:registers, registers}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -59,7 +61,7 @@ defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
:ok =
|
:ok =
|
||||||
PubSub.broadcast!(
|
PubSub.broadcast!(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
@installation_topic <> to_string(device.id),
|
@installation_topic <> to_string(Device.record(device, :id)),
|
||||||
{:opstat, opstat}
|
{:opstat, opstat}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,8 +40,7 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_continue({:init, installation}, state) do
|
def handle_continue({:init, installation}, state) do
|
||||||
user = AuthServer.get_auth(state.auth_server)
|
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)
|
:ok = Device.PubSub.broadcast_device(device)
|
||||||
|
|
||||||
state =
|
state =
|
||||||
|
@ -75,7 +74,7 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
|
||||||
user = AuthServer.get_auth(state.auth_server)
|
user = AuthServer.get_auth(state.auth_server)
|
||||||
|
|
||||||
Logger.debug("Begin set temp to #{temp}")
|
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)}")
|
Logger.debug("Set temp result: #{inspect(resp)}")
|
||||||
{:reply, resp, state}
|
{:reply, resp, state}
|
||||||
end
|
end
|
||||||
|
@ -119,11 +118,15 @@ defmodule GeoTherminator.PumpAPI.Device.Server do
|
||||||
|
|
||||||
[status, registers, opstat] =
|
[status, registers, opstat] =
|
||||||
Task.async_stream(
|
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),
|
& &1.(user, state.device),
|
||||||
timeout: Application.fetch_env!(:geo_therminator, :api_timeout)
|
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_status(state.device, status)
|
||||||
Device.PubSub.broadcast_registers(state.device, registers)
|
Device.PubSub.broadcast_registers(state.device, registers)
|
||||||
|
|
|
@ -1,19 +1,51 @@
|
||||||
defmodule GeoTherminatorWeb.Components.MainView do
|
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
|
use GeoTherminatorWeb, :live_component
|
||||||
|
|
||||||
|
alias GeoTherminator.PumpAPI.Device
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
{:ok,
|
{:ok,
|
||||||
assign(socket,
|
assign(socket,
|
||||||
set_temp: assigns.status.heating_effect,
|
set_temp: Device.Status.record(assigns.status, :heating_effect),
|
||||||
set_temp_active: assigns.status.is_heating_effect_set_by_user,
|
set_temp_active: Device.Status.record(assigns.status, :is_heating_effect_set_by_user),
|
||||||
hot_water_temp: assigns.registers.hot_water_temp.register_value,
|
hot_water_temp:
|
||||||
brine_out: assigns.registers.brine_out.register_value,
|
Device.Register.record(
|
||||||
brine_in: assigns.registers.brine_in.register_value,
|
Device.RegisterCollection.record(assigns.registers, :hot_water_temp),
|
||||||
supply_out: assigns.registers.supply_out.register_value,
|
:value
|
||||||
supply_in: assigns.registers.supply_in.register_value,
|
),
|
||||||
outdoor_temp: assigns.registers.outdoor_temp.register_value,
|
brine_out:
|
||||||
priority: assigns.opstat.priority
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
defmodule GeoTherminatorWeb.MainLive.DeviceList do
|
defmodule GeoTherminatorWeb.MainLive.DeviceList do
|
||||||
|
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
use GeoTherminatorWeb, :live_view
|
use GeoTherminatorWeb, :live_view
|
||||||
|
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
|
||||||
@impl Phoenix.LiveView
|
@impl Phoenix.LiveView
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
|
@ -8,8 +8,14 @@
|
||||||
<ul>
|
<ul>
|
||||||
<%= for installation <- @installations do %>
|
<%= for installation <- @installations do %>
|
||||||
<li>
|
<li>
|
||||||
<%= live_redirect(installation.id,
|
<%= live_redirect(
|
||||||
to: Routes.live_path(@socket, GeoTherminatorWeb.MainLive.Pump, installation.id)
|
InstallationInfo.record(installation, :id),
|
||||||
|
to:
|
||||||
|
Routes.live_path(
|
||||||
|
@socket,
|
||||||
|
GeoTherminatorWeb.MainLive.Pump,
|
||||||
|
InstallationInfo.record(installation, :id)
|
||||||
|
)
|
||||||
) %>
|
) %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -58,7 +58,7 @@ defmodule GeoTherminatorWeb.MainLive.Index do
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
Logger.debug("Error starting auth server: #{inspect(err)}")
|
Logger.debug("Error starting auth server: #{inspect(err)}")
|
||||||
assign(socket, error: true)
|
assign(socket, error: err)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<label>Password <input type="password" name="password" /></label>
|
<label>Password <input type="password" name="password" /></label>
|
||||||
<button>Log in</button>
|
<button>Log in</button>
|
||||||
|
|
||||||
<%= if @error do %>
|
<%= if @error != false do %>
|
||||||
<div class="error">Error occurred, please try again.</div>
|
<div class="error">Error occurred, please try again: <%= inspect(@error) %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</form>
|
</form>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule GeoTherminatorWeb.MainLive.Pump do
|
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
|
use GeoTherminatorWeb, :live_view
|
||||||
alias GeoTherminator.PumpAPI.Auth
|
alias GeoTherminator.PumpAPI.Auth
|
||||||
alias GeoTherminator.PumpAPI.Device
|
alias GeoTherminator.PumpAPI.Device
|
||||||
|
@ -11,10 +16,10 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
|
||||||
info when info != nil <- Auth.Server.get_installation(Auth.Server, id),
|
info when info != nil <- Auth.Server.get_installation(Auth.Server, id),
|
||||||
:ok <- Device.PubSub.subscribe_installation(info),
|
:ok <- Device.PubSub.subscribe_installation(info),
|
||||||
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
|
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
|
||||||
%Device{} = device <- Device.Server.get_device(pid),
|
device <- Device.Server.get_device(pid),
|
||||||
%Device.Status{} = status <- Device.Server.get_status(pid),
|
status <- Device.Server.get_status(pid),
|
||||||
%Device.RegisterCollection{} = registers <- Device.Server.get_registers(pid),
|
registers <- Device.Server.get_registers(pid),
|
||||||
%Device.OpStat{} = opstat <- Device.Server.get_opstat(pid) do
|
opstat <- Device.Server.get_opstat(pid) do
|
||||||
CubDB.put(GeoTherminator.DB, :viewing_pump, str_id)
|
CubDB.put(GeoTherminator.DB, :viewing_pump, str_id)
|
||||||
|
|
||||||
assign(socket,
|
assign(socket,
|
||||||
|
@ -37,14 +42,15 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
|
||||||
def handle_event(event, unsigned_params, socket)
|
def handle_event(event, unsigned_params, socket)
|
||||||
|
|
||||||
def handle_event("inc_temp", _params, socket) do
|
def handle_event("inc_temp", _params, socket) do
|
||||||
if not socket.assigns.status.is_heating_effect_set_by_user do
|
if not Device.Status.record(socket.assigns.status, :is_heating_effect_set_by_user) do
|
||||||
current = socket.assigns.status.heating_effect
|
current = Device.Status.record(socket.assigns.status, :heating_effect)
|
||||||
_ = Device.Server.set_temp(socket.assigns.pid, current + 1)
|
_ = Device.Server.set_temp(socket.assigns.pid, current + 1)
|
||||||
|
|
||||||
optimistic_status = %Device.Status{
|
optimistic_status =
|
||||||
socket.assigns.status
|
Device.Status.record(
|
||||||
| is_heating_effect_set_by_user: true
|
socket.assigns.status,
|
||||||
}
|
is_heating_effect_set_by_user: true
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, assign(socket, status: optimistic_status)}
|
{:noreply, assign(socket, status: optimistic_status)}
|
||||||
else
|
else
|
||||||
|
@ -53,14 +59,15 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("dec_temp", _params, socket) do
|
def handle_event("dec_temp", _params, socket) do
|
||||||
if not socket.assigns.status.is_heating_effect_set_by_user do
|
if not Device.Status.record(socket.assigns.status, :is_heating_effect_set_by_user) do
|
||||||
current = socket.assigns.status.heating_effect
|
current = Device.Status.record(socket.assigns.status, :heating_effect)
|
||||||
_ = Device.Server.set_temp(socket.assigns.pid, current - 1)
|
_ = Device.Server.set_temp(socket.assigns.pid, current - 1)
|
||||||
|
|
||||||
optimistic_status = %Device.Status{
|
optimistic_status =
|
||||||
socket.assigns.status
|
Device.Status.record(
|
||||||
| is_heating_effect_set_by_user: true
|
socket.assigns.status,
|
||||||
}
|
is_heating_effect_set_by_user: true
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, assign(socket, status: optimistic_status)}
|
{:noreply, assign(socket, status: optimistic_status)}
|
||||||
else
|
else
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<% else %>
|
<% else %>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={GeoTherminatorWeb.Components.MainView}
|
module={GeoTherminatorWeb.Components.MainView}
|
||||||
id={"pump-#{@device.id}"}
|
id={"pump-#{Device.record(@device, :id)}"}
|
||||||
device={@device}
|
device={@device}
|
||||||
status={@status}
|
status={@status}
|
||||||
registers={@registers}
|
registers={@registers}
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -76,7 +76,7 @@ defmodule GeoTherminator.MixProject do
|
||||||
{:finch, "~> 0.9.0"},
|
{:finch, "~> 0.9.0"},
|
||||||
{:desktop, "~> 1.4"},
|
{:desktop, "~> 1.4"},
|
||||||
{:cubdb, "~> 2.0"},
|
{:cubdb, "~> 2.0"},
|
||||||
{:gleam_stdlib, "~> 0.25"},
|
{:gleam_stdlib, "~> 0.26"},
|
||||||
{:gleam_http, "~> 3.1"},
|
{:gleam_http, "~> 3.1"},
|
||||||
{:gleam_erlang, "~> 0.17.1"},
|
{:gleam_erlang, "~> 0.17.1"},
|
||||||
{:gleam_json, "~> 0.5.0"}
|
{:gleam_json, "~> 0.5.0"}
|
||||||
|
|
2
mix.lock
2
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_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_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_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"},
|
"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"},
|
"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"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
//// Mostly translated from:
|
//// Mostly translated from:
|
||||||
//// https://github.com/klejejs/python-thermia-online-api/blob/2f0ec4e45bfecbd90932a10247283cbcd6a6c48c/ThermiaOnlineAPI/api/ThermiaAPI.py
|
//// https://github.com/klejejs/python-thermia-online-api/blob/2f0ec4e45bfecbd90932a10247283cbcd6a6c48c/ThermiaOnlineAPI/api/ThermiaAPI.py
|
||||||
//// Used under the Gnu General Public License 3.0
|
//// 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/json
|
||||||
import gleam/base
|
import gleam/base
|
||||||
|
@ -23,14 +26,6 @@ import helpers/parsing
|
||||||
|
|
||||||
const challenge_length = 43
|
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 = "
|
const b2c_authorize_prefix = "var SETTINGS = "
|
||||||
|
|
||||||
pub type Tokens {
|
pub type Tokens {
|
||||||
|
@ -74,10 +69,6 @@ pub fn authenticate(
|
||||||
get_tokens(confirmed, code_challenge)
|
get_tokens(confirmed, code_challenge)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(tokens: Tokens) -> Result(Tokens, B2CError) {
|
|
||||||
todo
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
||||||
let auth_data = [
|
let auth_data = [
|
||||||
#("response_type", "code"),
|
#("response_type", "code"),
|
||||||
|
@ -250,25 +241,28 @@ fn hash_challenge(challenge: String) -> String {
|
||||||
base.url_encode64(hashed, False)
|
base.url_encode64(hashed, False)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authorize_url() -> String {
|
fn authorize_url() -> uri.Uri {
|
||||||
b2c_auth_url <> "/oauth2/v2.0/authorize"
|
let url = config.api_url("b2c_auth_url")
|
||||||
|
uri.Uri(..url, path: url.path <> "/oauth2/v2.0/authorize")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn self_asserted_url() -> String {
|
fn self_asserted_url() -> uri.Uri {
|
||||||
b2c_auth_url <> "/SelfAsserted"
|
let url = config.api_url("b2c_auth_url")
|
||||||
|
uri.Uri(..url, path: url.path <> "/SelfAsserted")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_url() -> String {
|
fn confirm_url() -> uri.Uri {
|
||||||
b2c_auth_url <> "/api/CombinedSigninAndSignup/confirmed"
|
let url = config.api_url("b2c_auth_url")
|
||||||
|
uri.Uri(..url, path: url.path <> "/api/CombinedSigninAndSignup/confirmed")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_token_url() -> String {
|
fn get_token_url() -> uri.Uri {
|
||||||
b2c_auth_url <> "/oauth2/v2.0/token"
|
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) {
|
fn build_req(url: uri.Uri, method: http.Method) -> request.Request(String) {
|
||||||
assert Ok(req_url) = uri.parse(url)
|
assert Ok(req) = request.from_uri(url)
|
||||||
assert Ok(req) = request.from_uri(req_url)
|
|
||||||
|
|
||||||
let req = request.set_method(req, method)
|
let req = request.set_method(req, method)
|
||||||
|
|
||||||
|
@ -304,10 +298,12 @@ fn run_req(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_request_data() -> List(#(String, String)) {
|
fn base_request_data() -> List(#(String, String)) {
|
||||||
|
let b2c_client_id = config.str("b2c_client_id")
|
||||||
|
|
||||||
[
|
[
|
||||||
#("client_id", b2c_client_id),
|
#("client_id", b2c_client_id),
|
||||||
#("scope", b2c_scope),
|
#("scope", b2c_client_id),
|
||||||
#("redirect_uri", b2c_redirect_uri),
|
#("redirect_uri", config.str("b2c_redirect_url")),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,24 +18,35 @@ pub fn api_timeout() -> Int {
|
||||||
timeout_int
|
timeout_int
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn api_auth_url() -> uri.Uri {
|
pub fn api_url(config_key: String) -> uri.Uri {
|
||||||
config_url("api_auth_url")
|
map_api_url(config_key, fn(s) { s })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn api_installations_url() -> uri.Uri {
|
pub fn map_api_url(config_key: String, mapper: fn(String) -> String) -> uri.Uri {
|
||||||
config_url("api_installations_url")
|
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 {
|
fn app_name() -> atom.Atom {
|
||||||
assert Ok(name) = atom.from_string("geo_therminator")
|
assert Ok(name) = atom.from_string("geo_therminator")
|
||||||
name
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,38 @@
|
||||||
|
import gleam
|
||||||
import gleam/erlang/atom.{Atom}
|
import gleam/erlang/atom.{Atom}
|
||||||
|
import gleam/dynamic
|
||||||
|
|
||||||
|
pub type FromDynamicError {
|
||||||
|
NotAString
|
||||||
|
ParseError
|
||||||
|
}
|
||||||
|
|
||||||
pub external type DateTime
|
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"
|
"Elixir.DateTime" "from_iso8601"
|
||||||
|
|
||||||
pub external fn from_unix(Int) -> Result(DateTime, #(Atom, Atom)) =
|
pub external fn from_unix(Int) -> Result(DateTime, #(Atom, Atom)) =
|
||||||
|
|
22
src/helpers/naive_date_time.gleam
Normal file
22
src/helpers/naive_date_time.gleam
Normal file
|
@ -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"
|
17
src/pump_api/api.gleam
Normal file
17
src/pump_api/api.gleam
Normal file
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import gleam/http/request
|
import gleam/http/request
|
||||||
import gleam/json
|
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
import gleam/list
|
import gleam/list
|
||||||
|
@ -8,18 +7,11 @@ import pump_api/auth/user.{User}
|
||||||
import pump_api/auth/tokens.{Tokens}
|
import pump_api/auth/tokens.{Tokens}
|
||||||
import pump_api/auth/installation_info.{InstallationInfo}
|
import pump_api/auth/installation_info.{InstallationInfo}
|
||||||
import pump_api/http
|
import pump_api/http
|
||||||
|
import pump_api/api.{ApiError, AuthError, InvalidData}
|
||||||
import helpers/config
|
import helpers/config
|
||||||
import helpers/date_time
|
import helpers/date_time
|
||||||
import helpers/parsing
|
import helpers/parsing
|
||||||
import helpers/finch
|
import azure/b2c
|
||||||
import azure/b2c.{B2CError}
|
|
||||||
|
|
||||||
pub type ApiError {
|
|
||||||
ApiRequestFailed
|
|
||||||
NotOkResponse
|
|
||||||
InvalidData(msg: String)
|
|
||||||
AuthError(inner: B2CError)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
|
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
|
||||||
try tokens =
|
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) {
|
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)
|
assert Ok(raw_req) = request.from_uri(url)
|
||||||
|
|
||||||
let empty_req = request.set_body(raw_req, http.Empty)
|
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 =
|
try items =
|
||||||
parsing.data_get(
|
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) }))
|
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.",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
56
src/pump_api/device.gleam
Normal file
56
src/pump_api/device.gleam
Normal file
|
@ -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))
|
||||||
|
}
|
286
src/pump_api/device/api.gleam
Normal file
286
src/pump_api/device/api.gleam
Normal file
|
@ -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"
|
|
@ -1,6 +1,10 @@
|
||||||
import gleam/http/request
|
import gleam/http/request
|
||||||
import gleam/json.{Json}
|
import gleam/json.{Json}
|
||||||
|
import gleam/result
|
||||||
|
import gleam/dynamic
|
||||||
import pump_api/auth/user.{User}
|
import pump_api/auth/user.{User}
|
||||||
|
import helpers/finch
|
||||||
|
import helpers/config
|
||||||
|
|
||||||
pub type Body {
|
pub type Body {
|
||||||
Empty
|
Empty
|
||||||
|
@ -11,6 +15,12 @@ pub type Body {
|
||||||
pub type ApiRequest =
|
pub type ApiRequest =
|
||||||
request.Request(Body)
|
request.Request(Body)
|
||||||
|
|
||||||
|
pub type ApiError {
|
||||||
|
RequestFailed
|
||||||
|
NotOkResponse
|
||||||
|
InvalidData
|
||||||
|
}
|
||||||
|
|
||||||
pub fn authed_req(user: User, r: ApiRequest) {
|
pub fn authed_req(user: User, r: ApiRequest) {
|
||||||
r
|
r
|
||||||
|> request.set_header("authorization", "Bearer " <> user.tokens.access_token)
|
|> 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))
|
|> 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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue