diff --git a/.gitignore b/.gitignore index 3939c37..78a2025 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ npm-debug.log src/geo_therminator.gleam .elixir_ls + +/old diff --git a/.tool-versions b/.tool-versions index 4ac13b4..b18103d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,2 @@ -erlang 25.0.4 -elixir 1.13.4-otp-25 -gleam 0.25.3 +gleam 0.30.5 +nodejs 18.16.0 diff --git a/assets/css/app.css b/assets/css/app.css deleted file mode 100644 index 9de23e2..0000000 --- a/assets/css/app.css +++ /dev/null @@ -1,110 +0,0 @@ -/* This file is for your main application CSS */ -@import "./phoenix.css"; - -/* Alerts and form errors used by phx.new */ -.alert { - padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; -} - -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} - -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} - -.alert-danger { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} - -.alert p { - margin-bottom: 0; -} - -.alert:empty { - display: none; -} - -.invalid-feedback { - color: #a94442; - display: block; - margin: -1rem 0 2rem; -} - -/* LiveView specific classes for your customization */ -.phx-no-feedback.invalid-feedback, -.phx-no-feedback .invalid-feedback { - display: none; -} - -.phx-click-loading { - opacity: 0.5; - transition: opacity 1s ease-out; -} - -.phx-disconnected { - cursor: wait; -} - -.phx-disconnected * { - pointer-events: none; -} - -html, -body, -div[data-phx-main="true"], -main.container, -.main-view-component { - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -.gthm-logo { - min-width: 300px; - margin: 1rem; - display: block; -} - -.gthm-logo img { - width: auto; - display: block; - max-height: 100px; -} - -.main-view-component { - position: relative; - - padding: 10px; -} - -.main-view-component svg { - width: 100%; - height: 100%; - object-fit: contain; - - margin: 0 auto; -} - -.pump-btn:hover { - cursor: pointer; -} - -.pump-btn-loading:hover { - cursor: progress; -} - -.page-container { - padding: 10px; -} diff --git a/assets/css/phoenix.css b/assets/css/phoenix.css deleted file mode 100644 index 8e842ab..0000000 --- a/assets/css/phoenix.css +++ /dev/null @@ -1,76 +0,0 @@ -/* Includes some default style for the starter application. - * This can be safely deleted to start fresh. - */ - -/* Milligram v1.4.1 https://milligram.github.io - * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license - */ - -*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} - -/* General style */ -h1{font-size: 3.6rem; line-height: 1.25} -h2{font-size: 2.8rem; line-height: 1.3} -h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} -h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} -h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} -h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} -pre{padding: 1em;} - -.container{ - margin: 0 auto; - position: relative; - width: 100% -} -select { - width: auto; -} - - -/* Headers */ -header { - width: 100%; - background: #fdfdfd; - border-bottom: 1px solid #eaeaea; - margin-bottom: 2rem; -} -header section { - align-items: center; - display: flex; - flex-direction: column; - justify-content: space-between; -} -header section :first-child { - order: 2; -} -header section :last-child { - order: 1; -} -header nav ul, -header nav li { - margin: 0; - padding: 0; - display: block; - text-align: right; - white-space: nowrap; -} -header nav ul { - margin: 1rem; - margin-top: 0; -} -header nav a { - display: block; -} - -@media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ - header section { - flex-direction: row; - } - header nav ul { - margin: 1rem; - } - .phx-logo { - flex-basis: 527px; - margin: 2rem 1rem; - } -} diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index 9eabcff..0000000 --- a/assets/js/app.js +++ /dev/null @@ -1,44 +0,0 @@ -// We import the CSS which is extracted to its own file by esbuild. -// Remove this line if you add a your own CSS build pipeline (e.g postcss). -import "../css/app.css" - -// If you want to use Phoenix channels, run `mix help phx.gen.channel` -// to get started and then uncomment the line below. -// import "./user_socket.js" - -// You can include dependencies in two ways. -// -// The simplest option is to put them in assets/vendor and -// import them using relative paths: -// -// import "./vendor/some-package.js" -// -// Alternatively, you can `npm install some-package` and import -// them using a path starting with the package name: -// -// import "some-package" -// - -// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html" -// Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" -import topbar from "../vendor/topbar" - -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) - -// Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) -window.addEventListener("phx:page-loading-start", info => topbar.show()) -window.addEventListener("phx:page-loading-stop", info => topbar.hide()) - -// connect if there are any LiveViews on the page -liveSocket.connect() - -// expose liveSocket on window for web console debug logs and latency simulation: -// >> liveSocket.enableDebug() -// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session -// >> liveSocket.disableLatencySim() -window.liveSocket = liveSocket diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js deleted file mode 100644 index ff7fbb6..0000000 --- a/assets/vendor/topbar.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @license MIT - * topbar 1.0.0, 2021-01-06 - * http://buunguyen.github.io/topbar - * Copyright (c) 2021 Buu Nguyen - */ -(function (window, document) { - "use strict"; - - // https://gist.github.com/paulirish/1579671 - (function () { - var lastTime = 0; - var vendors = ["ms", "moz", "webkit", "o"]; - for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = - window[vendors[x] + "RequestAnimationFrame"]; - window.cancelAnimationFrame = - window[vendors[x] + "CancelAnimationFrame"] || - window[vendors[x] + "CancelRequestAnimationFrame"]; - } - if (!window.requestAnimationFrame) - window.requestAnimationFrame = function (callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function () { - callback(currTime + timeToCall); - }, timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - if (!window.cancelAnimationFrame) - window.cancelAnimationFrame = function (id) { - clearTimeout(id); - }; - })(); - - var canvas, - progressTimerId, - fadeTimerId, - currentProgress, - showing, - addEvent = function (elem, type, handler) { - if (elem.addEventListener) elem.addEventListener(type, handler, false); - else if (elem.attachEvent) elem.attachEvent("on" + type, handler); - else elem["on" + type] = handler; - }, - options = { - autoRun: true, - barThickness: 3, - barColors: { - 0: "rgba(26, 188, 156, .9)", - ".25": "rgba(52, 152, 219, .9)", - ".50": "rgba(241, 196, 15, .9)", - ".75": "rgba(230, 126, 34, .9)", - "1.0": "rgba(211, 84, 0, .9)", - }, - shadowBlur: 10, - shadowColor: "rgba(0, 0, 0, .6)", - className: null, - }, - repaint = function () { - canvas.width = window.innerWidth; - canvas.height = options.barThickness * 5; // need space for shadow - - var ctx = canvas.getContext("2d"); - ctx.shadowBlur = options.shadowBlur; - ctx.shadowColor = options.shadowColor; - - var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); - for (var stop in options.barColors) - lineGradient.addColorStop(stop, options.barColors[stop]); - ctx.lineWidth = options.barThickness; - ctx.beginPath(); - ctx.moveTo(0, options.barThickness / 2); - ctx.lineTo( - Math.ceil(currentProgress * canvas.width), - options.barThickness / 2 - ); - ctx.strokeStyle = lineGradient; - ctx.stroke(); - }, - createCanvas = function () { - canvas = document.createElement("canvas"); - var style = canvas.style; - style.position = "fixed"; - style.top = style.left = style.right = style.margin = style.padding = 0; - style.zIndex = 100001; - style.display = "none"; - if (options.className) canvas.classList.add(options.className); - document.body.appendChild(canvas); - addEvent(window, "resize", repaint); - }, - topbar = { - config: function (opts) { - for (var key in opts) - if (options.hasOwnProperty(key)) options[key] = opts[key]; - }, - show: function () { - if (showing) return; - showing = true; - if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); - if (!canvas) createCanvas(); - canvas.style.opacity = 1; - canvas.style.display = "block"; - topbar.progress(0); - if (options.autoRun) { - (function loop() { - progressTimerId = window.requestAnimationFrame(loop); - topbar.progress( - "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) - ); - })(); - } - }, - progress: function (to) { - if (typeof to === "undefined") return currentProgress; - if (typeof to === "string") { - to = - (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 - ? currentProgress - : 0) + parseFloat(to); - } - currentProgress = to > 1 ? 1 : to; - repaint(); - return currentProgress; - }, - hide: function () { - if (!showing) return; - showing = false; - if (progressTimerId != null) { - window.cancelAnimationFrame(progressTimerId); - progressTimerId = null; - } - (function loop() { - if (topbar.progress("+.1") >= 1) { - canvas.style.opacity -= 0.05; - if (canvas.style.opacity <= 0.05) { - canvas.style.display = "none"; - fadeTimerId = null; - return; - } - } - fadeTimerId = window.requestAnimationFrame(loop); - })(); - }, - }; - - if (typeof module === "object" && typeof module.exports === "object") { - module.exports = topbar; - } else if (typeof define === "function" && define.amd) { - define(function () { - return topbar; - }); - } else { - this.topbar = topbar; - } -}.call(this, window, document)); diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index 79eeb0a..0000000 --- a/config/config.exs +++ /dev/null @@ -1,37 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Config module. -# -# This configuration file is loaded before any dependency and -# is restricted to this project. - -# General application configuration -import Config - -# Configures the endpoint -config :geo_therminator, GeoTherminatorWeb.Endpoint, - url: [host: "localhost"], - render_errors: [view: GeoTherminatorWeb.ErrorView, accepts: ~w(html json), layout: false], - pubsub_server: GeoTherminator.PubSub, - live_view: [signing_salt: "PC6YJv8v"] - -# Configure esbuild (the version is required) -config :esbuild, - version: "0.12.18", - default: [ - args: - ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), - cd: Path.expand("../assets", __DIR__), - env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} - ] - -# Configures Elixir's Logger -config :logger, :console, - format: "$time $metadata[$level] $message\n", - metadata: [:request_id] - -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason - -# Import environment specific config. This must remain at the bottom -# of this file so it overrides the configuration defined above. -import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs deleted file mode 100644 index fa8ec49..0000000 --- a/config/dev.exs +++ /dev/null @@ -1,65 +0,0 @@ -import Config - -# For development, we disable any cache and enable -# debugging and code reloading. -# -# The watchers configuration can be used to run external -# watchers to your application. For example, we use it -# with esbuild to bundle .js and .css sources. -config :geo_therminator, GeoTherminatorWeb.Endpoint, - # Binding to loopback ipv4 address prevents access from other machines. - # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], - check_origin: false, - code_reloader: true, - debug_errors: true, - secret_key_base: "iNdGHrsVxsC/iRd6ItQmU6WUOOgbs8lixEMdKIypmM2NuKb1PPrzncqR6ETXQvvR", - watchers: [ - # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) - esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} - ] - -# ## SSL Support -# -# In order to use HTTPS in development, a self-signed -# certificate can be generated by running the following -# Mix task: -# -# mix phx.gen.cert -# -# Note that this task requires Erlang/OTP 20 or later. -# Run `mix help phx.gen.cert` for more information. -# -# The `http:` config above can be replaced with: -# -# https: [ -# port: 4001, -# cipher_suite: :strong, -# keyfile: "priv/cert/selfsigned_key.pem", -# certfile: "priv/cert/selfsigned.pem" -# ], -# -# If desired, both `http:` and `https:` keys can be -# configured to run both http and https servers on -# different ports. - -# Watch static and templates for browser reloading. -config :geo_therminator, GeoTherminatorWeb.Endpoint, - live_reload: [ - patterns: [ - ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", - ~r"priv/gettext/.*(po)$", - ~r"lib/geo_therminator_web/(live|views)/.*(ex)$", - ~r"lib/geo_therminator_web/templates/.*(eex)$" - ] - ] - -# Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" - -# Set a higher stacktrace during development. Avoid configuring such -# in production as building large stacktraces may be expensive. -config :phoenix, :stacktrace_depth, 20 - -# Initialize plugs at runtime for faster development compilation -config :phoenix, :plug_init_mode, :runtime diff --git a/config/prod.exs b/config/prod.exs deleted file mode 100644 index f79b8b5..0000000 --- a/config/prod.exs +++ /dev/null @@ -1,50 +0,0 @@ -import Config - -# For production, don't forget to configure the url host -# to something meaningful, Phoenix uses this information -# when generating URLs. -# -# Note we also include the path to a cache manifest -# containing the digested version of static files. This -# manifest is generated by the `mix phx.digest` task, -# which you should run after static files are built and -# before starting your production server. -config :geo_therminator, GeoTherminatorWeb.Endpoint, - cache_static_manifest: "priv/static/cache_manifest.json" - -# Do not print debug messages in production -config :logger, level: :info - -# ## SSL Support -# -# To get SSL working, you will need to add the `https` key -# to the previous section and set your `:url` port to 443: -# -# config :geo_therminator, GeoTherminatorWeb.Endpoint, -# ..., -# url: [host: "example.com", port: 443], -# https: [ -# ..., -# port: 443, -# cipher_suite: :strong, -# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), -# certfile: System.get_env("SOME_APP_SSL_CERT_PATH") -# ] -# -# The `cipher_suite` is set to `:strong` to support only the -# latest and more secure SSL ciphers. This means old browsers -# and clients may not be supported. You can set it to -# `:compatible` for wider support. -# -# `:keyfile` and `:certfile` expect an absolute path to the key -# and cert in disk or a relative path inside priv, for example -# "priv/ssl/server.key". For all supported SSL configuration -# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 -# -# We also recommend setting `force_ssl` in your endpoint, ensuring -# no data is ever sent via http, always redirecting to https: -# -# config :geo_therminator, GeoTherminatorWeb.Endpoint, -# force_ssl: [hsts: true] -# -# Check `Plug.SSL` for all available options in `force_ssl`. diff --git a/config/runtime.exs b/config/runtime.exs deleted file mode 100644 index 6a7c95c..0000000 --- a/config/runtime.exs +++ /dev/null @@ -1,133 +0,0 @@ -import Config -import GeoTherminator.ConfigHelpers - -Application.app_dir(:geo_therminator, "priv") -|> Path.join(".env") -|> DotenvParser.load_file() - -config :geo_therminator, - api_timeout: 30_000, - api_auth_url: - get_env("API_AUTH_URL", "https://thermia-auth-api.azurewebsites.net/api/v1/Jwt/login"), - api_installations_url: - get_env( - "API_INSTALLATIONS_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installationsInfo" - ), - api_device_url: - get_env( - "API_DEVICE_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installations/{id}" - ), - api_device_status_url: - get_env( - "API_DEVICE_STATUS_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installationstatus/{id}/status" - ), - api_device_register_url: - get_env( - "API_DEVICE_REGISTER_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Groups/REG_GROUP_TEMPERATURES" - ), - api_device_opstat_url: - get_env( - "API_DEVICE_OPSTAT_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Groups/REG_GROUP_OPERATIONAL_STATUS" - ), - api_opstat_mapping: %{ - # "REG_VALUE_STATUS_MANUAL" => 1, - 1 => :hand_operated, - # "REG_VALUE_STATUS_TAPWATER" => 3, - 3 => :hot_water, - # "REG_VALUE_STATUS_HEAT" => 4, - 4 => :heating, - # "REG_VALUE_STATUS_COOL" => 5, - 5 => :active_cooling, - # "REG_VALUE_STATUS_POOL" => 6, - 6 => :pool, - # "REG_VALUE_STATUS_LEGIONELLA" => 7, - 7 => :anti_legionella, - # "REG_VALUE_STATUS_PASSIVE_COOL" => 8, - 8 => :passive_cooling, - # "REG_VALUE_STATUS_STANDBY" => 98, - 98 => :standby, - # "REG_VALUE_STATUS_IDLE" => 99, - 99 => :idle, - # "REG_VALUE_STATUS_OFF" => 100 - 100 => :off - }, - api_opstat_bitmask_mapping: %{ - 1 => :hand_operated, - 2 => :defrost, - 4 => :hot_water, - 8 => :heating, - 16 => :active_cooling, - 32 => :pool, - 64 => :anti_legionella, - 128 => :passive_cooling, - 512 => :standby, - 1024 => :idle, - 2048 => :off - }, - api_device_reg_set_url: - get_env( - "API_DEVICE_REG_SET_URL", - "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Registers" - ), - 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, - - # Database directory for settings - db_dir: - Path.join( - if config_target() in [:ios, :android] do - System.get_env("HOME") - else - Application.app_dir(:geo_therminator, "priv") - end, - get_env("DB_DIR", "db") - ) - -if config_env() == :dev do - config :geo_therminator, GeoTherminatorWeb.Endpoint, - http: [ - port: 0 - ] -end - -if config_env() == :prod do - # The secret key base is used to sign/encrypt cookies and other secrets. - # A default value is used in config/dev.exs and config/test.exs but you - # want to use a different value for prod and you most likely don't want - # to check this value into version control, so we use an environment - # variable instead. - secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ - - config :geo_therminator, GeoTherminatorWeb.Endpoint, - http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. - # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {127, 0, 0, 1}, - port: 0 - ], - secret_key_base: secret_key_base, - server: true - - # ## Using releases - # - # If you are doing OTP releases, you need to instruct Phoenix - # to start each relevant endpoint: - # - # config :geo_therminator, GeoTherminatorWeb.Endpoint, server: true - # - # Then you can assemble a release by calling `mix release`. - # See `mix help release` for more information. -end diff --git a/config/test.exs b/config/test.exs deleted file mode 100644 index 8bba849..0000000 --- a/config/test.exs +++ /dev/null @@ -1,14 +0,0 @@ -import Config - -# We don't run a server during test. If one is required, -# you can enable the server option below. -config :geo_therminator, GeoTherminatorWeb.Endpoint, - http: [ip: {127, 0, 0, 1}, port: 4002], - secret_key_base: "jTM8H/r05f0LEiokDoSBgftJugkpUkfr/0wpwwISIZ6wOOM+90aKvcAF8G21pNKA", - server: false - -# Print only warnings and errors during test -config :logger, level: :warn - -# Initialize plugs at runtime for faster test compilation -config :phoenix, :plug_init_mode, :runtime diff --git a/gleam.toml b/gleam.toml index 4683f3c..837717d 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,10 +1,10 @@ name = "geo_therminator" -version = "0.3.0" -target = "erlang" +version = "1.0.0" +target = "javascript" [dependencies] -gleam_stdlib = "~> 0.25" -gleam_http = "~> 3.1" -gleam_hackney = "~> 0.2.1" -gleam_erlang = "~> 0.17.1" -gleam_json = "~> 0.5.0" +gleam_stdlib = "~> 0.30" +gleam_http = "~> 3.5" +gleam_json = "~> 0.6.0" +gleam_javascript = "~> 0.6.0" +gleam_fetch = "~> 0.2.0" diff --git a/lib/geo_therminator/application.ex b/lib/geo_therminator/application.ex deleted file mode 100644 index c144925..0000000 --- a/lib/geo_therminator/application.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule GeoTherminator.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - - use Application - - @impl true - def start(_type, _args) do - db_dir = Application.get_env(:geo_therminator, :db_dir) - - children = [ - {CubDB, name: GeoTherminator.DB, data_dir: db_dir}, - {Finch, name: GeoTherminator.PumpAPI.HTTP}, - {Phoenix.PubSub, name: GeoTherminator.PumpAPI.Device.PubSub}, - {Registry, keys: :unique, name: GeoTherminator.PumpAPI.Device.Registry}, - {DynamicSupervisor, strategy: :one_for_one, name: GeoTherminator.PumpAPI.Device.Supervisor}, - {DynamicSupervisor, - strategy: :one_for_one, name: GeoTherminator.PumpAPI.Auth.Server.Supervisor}, - - # Start the Telemetry supervisor - GeoTherminatorWeb.Telemetry, - # Start the PubSub system - Supervisor.child_spec({Phoenix.PubSub, name: GeoTherminator.PubSub}, id: :phoenix_pubsub), - # Start the Endpoint (http/https) - GeoTherminatorWeb.Endpoint, - {Desktop.Window, - [ - app: :geo_therminator, - id: GeoTherminatorWindow, - url: &GeoTherminatorWeb.Endpoint.url/0 - ]} - ] - - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: GeoTherminator.Supervisor] - Supervisor.start_link(children, opts) - end - - # Tell Phoenix to update the endpoint configuration - # whenever the application is updated. - @impl true - def config_change(changed, _new, removed) do - GeoTherminatorWeb.Endpoint.config_change(changed, removed) - :ok - end -end diff --git a/lib/geo_therminator/config_helpers.ex b/lib/geo_therminator/config_helpers.ex deleted file mode 100644 index 8c85375..0000000 --- a/lib/geo_therminator/config_helpers.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule GeoTherminator.ConfigHelpers do - @type config_type :: :str | :int | :bool | :json - - @doc """ - Get value from environment variable, converting it to the given type if needed. - - If no default value is given, or `:no_default` is given as the default, an error is raised if the variable is not - set. - """ - @spec get_env(String.t(), :no_default | any(), config_type()) :: any() - def get_env(var, default \\ :no_default, type \\ :str) - - def get_env(var, :no_default, type) do - System.fetch_env!(var) - |> get_with_type(type) - end - - def get_env(var, default, type) do - with {:ok, val} <- System.fetch_env(var) do - get_with_type(val, type) - else - :error -> default - end - end - - @spec get_with_type(String.t(), config_type()) :: any() - defp get_with_type(val, type) - - defp get_with_type(val, :str), do: val - defp get_with_type(val, :int), do: String.to_integer(val) - defp get_with_type("true", :bool), do: true - defp get_with_type("false", :bool), do: false - defp get_with_type(val, :json), do: Jason.decode!(val) - defp get_with_type(val, type), do: raise("Cannot convert to #{inspect(type)}: #{inspect(val)}") -end diff --git a/lib/geo_therminator/pump_api/auth/installation_info.ex b/lib/geo_therminator/pump_api/auth/installation_info.ex deleted file mode 100644 index cd52047..0000000 --- a/lib/geo_therminator/pump_api/auth/installation_info.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Auth.InstallationInfo do - require Record - - Record.defrecord(:record, :installation_info, [:id]) - - @type t :: record(:record, id: non_neg_integer()) -end diff --git a/lib/geo_therminator/pump_api/auth/server.ex b/lib/geo_therminator/pump_api/auth/server.ex deleted file mode 100644 index 6662abd..0000000 --- a/lib/geo_therminator/pump_api/auth/server.ex +++ /dev/null @@ -1,127 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Auth.Server do - require Logger - require GeoTherminator.PumpAPI.Auth.User - require GeoTherminator.PumpAPI.Auth.InstallationInfo - require GeoTherminator.PumpAPI.Auth.Tokens - - use GenServer - import GeoTherminator.TypedStruct - alias GeoTherminator.PumpAPI.Auth - - @token_check_timer 10_000 - @token_check_diff 5 * 60 - - defmodule Options do - deftypedstruct(%{ - server_name: GenServer.name(), - username: String.t(), - password: String.t() - }) - end - - defmodule State do - deftypedstruct(%{ - username: String.t(), - password: String.t(), - authed_user: {Auth.User.t() | nil, nil}, - installations_fetched: {boolean(), false}, - installations: {[Auth.InstallationInfo.t()], []} - }) - end - - @spec start_link(Options.t()) :: GenServer.on_start() - def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: opts.server_name) - end - - @impl true - def init(%Options{} = opts) do - case init_state(opts.username, opts.password) do - {:ok, state} -> {:ok, state} - :error -> {:stop, :error} - end - end - - @impl true - def handle_call(msg, from, state) - - def handle_call(:get_auth, _from, state) do - {:reply, state.authed_user, state} - end - - def handle_call(:get_installations, _from, state) do - {:reply, state.installations, state} - end - - def handle_call({:get_installation, id}, _from, state) do - {:reply, Enum.find(state.installations, &(Auth.InstallationInfo.record(&1, :id) == id)), - state} - end - - @impl true - def handle_info(msg, state) - - def handle_info(:token_check, state) do - now = DateTime.utc_now() - - diff = - DateTime.diff( - state.authed_user - |> Auth.User.record(:tokens) - |> Auth.Tokens.record(:access_token_expiry), - now - ) - - schedule_token_check() - - if diff < @token_check_diff do - Logger.debug("Renewing auth token since #{diff} < #{@token_check_diff}") - - case init_state(state.username, state.password) do - {:ok, new_state} -> {:noreply, new_state} - :error -> {:noreply, state} - end - else - {:noreply, state} - end - end - - @spec get_auth(GenServer.name()) :: Auth.User.t() - def get_auth(server) do - GenServer.call(server, :get_auth) - end - - @spec get_installations(GenServer.name()) :: Auth.InstallationInfo.t() - def get_installations(server) do - GenServer.call(server, :get_installations) - end - - @spec get_installation(GenServer.name(), integer()) :: Auth.InstallationInfo.t() | nil - def get_installation(server, id) do - GenServer.call(server, {:get_installation, id}) - end - - defp schedule_token_check() do - Process.send_after(self(), :token_check, @token_check_timer) - end - - @spec init_state(String.t(), String.t()) :: {:ok, State.t()} | {:stop, :error} - defp init_state(username, password) do - with {:ok, user} <- :pump_api@auth@api.auth(username, password), - {:ok, installations} <- :pump_api@auth@api.installation_info(user) do - schedule_token_check() - - state = %State{ - authed_user: user, - installations: installations, - installations_fetched: true, - username: username, - password: password - } - - {:ok, state} - else - _ -> :error - end - end -end diff --git a/lib/geo_therminator/pump_api/auth/tokens.ex b/lib/geo_therminator/pump_api/auth/tokens.ex deleted file mode 100644 index af3f0d4..0000000 --- a/lib/geo_therminator/pump_api/auth/tokens.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Auth.Tokens do - require Record - - Record.defrecord(:record, :tokens, [ - :access_token, - :access_token_expiry, - :refresh_token, - :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() - ) -end diff --git a/lib/geo_therminator/pump_api/auth/user.ex b/lib/geo_therminator/pump_api/auth/user.ex deleted file mode 100644 index a09d577..0000000 --- a/lib/geo_therminator/pump_api/auth/user.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Auth.User do - require Record - - Record.defrecord(:record, :user, [:tokens]) - - @type t :: record(:record, tokens: GeoTherminator.PumpAPI.Auth.Tokens.t()) -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 deleted file mode 100644 index 9310a89..0000000 --- a/lib/geo_therminator/pump_api/device/device.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Device do - import GeoTherminator.TypedStruct - - 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() - }) - - defmodule Status do - deftypedstruct(%{ - heating_effect: integer(), - is_heating_effect_set_by_user: boolean() - }) - end - - defmodule Register do - deftypedstruct(%{ - register_name: String.t(), - register_value: integer(), - timestamp: DateTime.t() - }) - 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() - }) - end - - defmodule OpStat do - deftypedstruct(%{ - priority: - :hand_operated - | :hot_water - | :heating - | :active_cooling - | :pool - | :anti_legionella - | :passive_cooling - | :standby - | :idle - | :off - | :defrost - | :unknown - }) - end - - @spec get_device_process(GenServer.name(), GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) :: - {:ok, GenServer.name()} | :error - def get_device_process(auth_server, installation) do - case DynamicSupervisor.start_child( - __MODULE__.Supervisor, - {__MODULE__.Server, - %__MODULE__.Server.Options{ - auth_server: auth_server, - installation: installation - }} - ) do - {:ok, pid} -> - {:ok, pid} - - {:error, {:already_started, pid}} -> - {:ok, pid} - - err -> - IO.inspect(err) - :error - end - end -end diff --git a/lib/geo_therminator/pump_api/device/pub_sub.ex b/lib/geo_therminator/pump_api/device/pub_sub.ex deleted file mode 100644 index 56a8d63..0000000 --- a/lib/geo_therminator/pump_api/device/pub_sub.ex +++ /dev/null @@ -1,66 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Device.PubSub do - require GeoTherminator.PumpAPI.Auth.InstallationInfo - - alias Phoenix.PubSub - alias GeoTherminator.PumpAPI.Auth.InstallationInfo - - @installation_topic "installation:" - - @spec subscribe_installation(InstallationInfo.t()) :: :ok - def subscribe_installation(installation) do - :ok = - PubSub.subscribe( - __MODULE__, - @installation_topic <> to_string(InstallationInfo.record(installation, :id)) - ) - end - - @spec broadcast_device(GeoTherminator.PumpAPI.Device.t()) :: :ok - def broadcast_device(device) do - :ok = - PubSub.broadcast!( - __MODULE__, - @installation_topic <> to_string(device.id), - {:device, device} - ) - end - - @spec broadcast_status( - GeoTherminator.PumpAPI.Device.t(), - GeoTherminator.PumpAPI.Device.Status.t() - ) :: :ok - def broadcast_status(device, status) do - :ok = - PubSub.broadcast!( - __MODULE__, - @installation_topic <> to_string(device.id), - {:status, status} - ) - end - - @spec broadcast_registers( - GeoTherminator.PumpAPI.Device.t(), - GeoTherminator.PumpAPI.Device.RegisterCollection.t() - ) :: :ok - def broadcast_registers(device, registers) do - :ok = - PubSub.broadcast!( - __MODULE__, - @installation_topic <> to_string(device.id), - {:registers, registers} - ) - end - - @spec broadcast_opstat( - GeoTherminator.PumpAPI.Device.t(), - GeoTherminator.PumpAPI.Device.OpStat.t() - ) :: :ok - def broadcast_opstat(device, opstat) do - :ok = - PubSub.broadcast!( - __MODULE__, - @installation_topic <> to_string(device.id), - {:opstat, opstat} - ) - end -end diff --git a/lib/geo_therminator/pump_api/device/server.ex b/lib/geo_therminator/pump_api/device/server.ex deleted file mode 100644 index c91f222..0000000 --- a/lib/geo_therminator/pump_api/device/server.ex +++ /dev/null @@ -1,139 +0,0 @@ -defmodule GeoTherminator.PumpAPI.Device.Server do - use GenServer - import GeoTherminator.TypedStruct - alias GeoTherminator.PumpAPI.Device - alias GeoTherminator.PumpAPI.Auth.InstallationInfo - alias GeoTherminator.PumpAPI.Auth.Server, as: AuthServer - require Logger - - defmodule Options do - deftypedstruct(%{ - auth_server: GenServer.name(), - installation: InstallationInfo.t() - }) - end - - defmodule State do - deftypedstruct(%{ - auth_server: GenServer.name(), - device: {Device.t() | nil, nil}, - status: {Device.Status.t() | nil, nil}, - registers: {Device.RegisterCollection.t() | nil, nil}, - opstat: {Device.OpStat.t() | nil, nil} - }) - end - - @spec start_link(Options.t()) :: GenServer.on_start() - def start_link(opts) do - GenServer.start_link(__MODULE__, opts, - name: {:via, Registry, {Device.Registry, opts.installation.id}} - ) - end - - @impl true - def init(%Options{} = opts) do - {:ok, %State{auth_server: opts.auth_server}, {:continue, {:init, opts.installation}}} - end - - @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.PubSub.broadcast_device(device) - - state = - %State{state | device: device} - |> refresh_status() - - schedule_status() - {:noreply, state} - end - - @impl true - def handle_call(msg, from, state) - - def handle_call(:get_device, _from, state) do - {:reply, state.device, state} - end - - def handle_call(:get_status, _from, state) do - {:reply, state.status, state} - end - - def handle_call(:get_registers, _from, state) do - {:reply, state.registers, state} - end - - def handle_call(:get_opstat, _from, state) do - {:reply, state.opstat, state} - end - - def handle_call({:set_temp, temp}, _from, state) do - user = AuthServer.get_auth(state.auth_server) - - Logger.debug("Begin set temp to #{temp}") - resp = Device.API.set_temp(user, state.device, temp) - Logger.debug("Set temp result: #{inspect(resp)}") - {:reply, resp, state} - end - - @impl true - def handle_info(msg, state) - - def handle_info(:refresh_status, state) do - state = refresh_status(state) - schedule_status() - {:noreply, state} - end - - @spec get_device(GenServer.name()) :: Device.t() - def get_device(server) do - GenServer.call(server, :get_device) - end - - @spec get_status(GenServer.name()) :: Device.Status.t() - def get_status(server) do - GenServer.call(server, :get_status) - end - - @spec get_registers(GenServer.name()) :: Device.RegisterCollection.t() - def get_registers(server) do - GenServer.call(server, :get_registers) - end - - @spec get_opstat(GenServer.name()) :: Device.OpStat.t() - def get_opstat(server) do - GenServer.call(server, :get_opstat) - end - - @spec set_temp(GenServer.name(), integer()) :: :ok | {:error, String.t()} - def set_temp(server, temp) do - GenServer.call(server, {:set_temp, temp}) - end - - defp refresh_status(state) do - user = AuthServer.get_auth(state.auth_server) - - [status, registers, opstat] = - Task.async_stream( - [&Device.API.status/2, &Device.API.register_info/2, &Device.API.opstat/2], - & &1.(user, state.device), - timeout: Application.get_env(:geo_therminator, :api_timeout) - ) - |> Enum.map(fn {:ok, val} -> val end) - - Device.PubSub.broadcast_status(state.device, status) - Device.PubSub.broadcast_registers(state.device, registers) - Device.PubSub.broadcast_opstat(state.device, opstat) - %State{state | status: status, registers: registers, opstat: opstat} - end - - defp schedule_status() do - Process.send_after( - self(), - :refresh_status, - Application.get_env(:geo_therminator, :api_refresh) - ) - end -end diff --git a/lib/geo_therminator/pump_api/http.ex b/lib/geo_therminator/pump_api/http.ex deleted file mode 100644 index 97e92a4..0000000 --- a/lib/geo_therminator/pump_api/http.ex +++ /dev/null @@ -1,59 +0,0 @@ -defmodule GeoTherminator.PumpAPI.HTTP do - require GeoTherminator.PumpAPI.Auth.User - require GeoTherminator.PumpAPI.Auth.Tokens - - alias GeoTherminator.PumpAPI.Auth.User - alias GeoTherminator.PumpAPI.Auth.Tokens - - @spec authed_req( - User.t(), - Finch.Request.method(), - Finch.Request.url(), - Finch.Request.headers(), - map() | nil, - Keyword.t() - ) :: - Finch.Request.t() - def authed_req(user, method, url, headers \\ [], body \\ nil, opts \\ []) do - headers = - headers - |> List.keystore( - "authorization", - 0, - {"authorization", "Bearer #{User.record(user, :tokens) |> Tokens.record(:access_token)}"} - ) - - req(method, url, headers, body, opts) - end - - @spec req( - Finch.Request.method(), - Finch.Request.url(), - Finch.Request.headers(), - map() | nil, - Keyword.t() - ) :: - Finch.Request.t() - def req(method, url, headers \\ [], body \\ nil, opts \\ []) do - headers = - headers - |> List.keystore("accept", 0, {"accept", "application/json"}) - - headers = - if not is_nil(body) do - List.keystore(headers, "content-type", 0, {"content-type", "application/json"}) - else - headers - end - - opts = Keyword.put(opts, :timeout, Application.get_env(:geo_therminator, :api_timeout)) - - Finch.build( - method, - url, - headers, - if(not is_nil(body), do: Jason.encode!(body)), - opts - ) - end -end diff --git a/lib/geo_therminator/typed_struct.ex b/lib/geo_therminator/typed_struct.ex deleted file mode 100644 index a61bae5..0000000 --- a/lib/geo_therminator/typed_struct.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmodule GeoTherminator.TypedStruct do - @doc """ - Create typed struct with a type, default values, and enforced keys. - - Input should be a map where the key names are names of the struct keys and values are the - field information. The value can be a typespec, in which case the field will be enforced, or - a 2-tuple of `{typespec, default_value}`, making the field unenforced. - - To prevent ambiguity, a value of `{typespec, :ts_enforced}` will be interpreted as enforced, - this will allow you to typespec a 2-tuple. - - NOTE: Due to the ambiguity removal technique above, `:ts_enforced` is not allowed as a default - value. - - Example: - - ```elixir - deftypedstruct(%{ - # Enforced with simple type - foo: integer(), - - # Enforced 2-tuple typed field, written like this to remove ambiguity - bar: {{String.t(), integer()}, :ts_enforced}, - - # Non-enforced field with default value - baz: {any(), ""} - }) - ``` - """ - defmacro deftypedstruct(fields) do - fields_list = - case fields do - {:%{}, _, flist} -> flist - _ -> raise ArgumentError, "Fields must be a map!" - end - - enforced_list = - fields_list - |> Enum.filter(fn - {_, {_, :ts_enforced}} -> true - {_, {_, _}} -> false - {_, _} -> true - end) - |> Enum.map(&elem(&1, 0)) - - field_specs = - Enum.map(fields_list, fn - {field, {typespec, :ts_enforced}} -> - {field, typespec} - - {field, {typespec, _}} -> - {field, typespec} - - {field, typespec} -> - {field, typespec} - end) - - field_vals = - Enum.map(fields_list, fn - {field, {_, :ts_enforced}} -> field - {field, {_, default}} -> {field, default} - {field, _} -> field - end) - - quote do - @type t :: %__MODULE__{unquote_splicing(field_specs)} - @enforce_keys unquote(enforced_list) - defstruct unquote(field_vals) - end - end -end diff --git a/lib/geo_therminator_web/endpoint.ex b/lib/geo_therminator_web/endpoint.ex deleted file mode 100644 index 6285471..0000000 --- a/lib/geo_therminator_web/endpoint.ex +++ /dev/null @@ -1,49 +0,0 @@ -defmodule GeoTherminatorWeb.Endpoint do - use Desktop.Endpoint, otp_app: :geo_therminator - - # The session will be stored in the cookie and signed, - # this means its contents can be read but not tampered with. - # Set :encryption_salt if you would also like to encrypt it. - @session_options [ - store: :cookie, - key: "_geo_therminator_key", - signing_salt: "j8JUzvur" - ] - - socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] - - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phx.digest - # when deploying your static files in production. - plug Plug.Static, - at: "/", - from: :geo_therminator, - gzip: false, - only: ~w(assets fonts images favicon.ico robots.txt) - - # Code reloading can be explicitly enabled under the - # :code_reloader configuration of your endpoint. - if code_reloading? do - socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket - plug Phoenix.LiveReloader - plug Phoenix.CodeReloader - end - - plug Phoenix.LiveDashboard.RequestLogger, - param_key: "request_logger", - cookie_key: "request_logger" - - plug Plug.RequestId - plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] - - plug Plug.Parsers, - parsers: [:urlencoded, :multipart, :json], - pass: ["*/*"], - json_decoder: Phoenix.json_library() - - plug Plug.MethodOverride - plug Plug.Head - plug Plug.Session, @session_options - plug GeoTherminatorWeb.Router -end diff --git a/lib/geo_therminator_web/geo_therminator_web.ex b/lib/geo_therminator_web/geo_therminator_web.ex deleted file mode 100644 index a090f80..0000000 --- a/lib/geo_therminator_web/geo_therminator_web.ex +++ /dev/null @@ -1,104 +0,0 @@ -defmodule GeoTherminatorWeb do - @moduledoc """ - The entrypoint for defining your web interface, such - as controllers, views, channels and so on. - - This can be used in your application as: - - use GeoTherminatorWeb, :controller - use GeoTherminatorWeb, :view - - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused - on imports, uses and aliases. - - Do NOT define functions inside the quoted expressions - below. Instead, define any helper function in modules - and import those modules here. - """ - - def controller do - quote do - use Phoenix.Controller, namespace: GeoTherminatorWeb - - import Plug.Conn - import GeoTherminatorWeb.Gettext - alias GeoTherminatorWeb.Router.Helpers, as: Routes - end - end - - def view do - quote do - use Phoenix.View, - root: "lib/geo_therminator_web/templates", - namespace: GeoTherminatorWeb - - # Import convenience functions from controllers - import Phoenix.Controller, - only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] - - # Include shared imports and aliases for views - unquote(view_helpers()) - end - end - - def live_view do - quote do - use Phoenix.LiveView, - layout: {GeoTherminatorWeb.LayoutView, "live.html"} - - unquote(view_helpers()) - end - end - - def live_component do - quote do - use Phoenix.LiveComponent - - unquote(view_helpers()) - end - end - - def router do - quote do - use Phoenix.Router - - import Plug.Conn - import Phoenix.Controller - import Phoenix.LiveView.Router - end - end - - def channel do - quote do - use Phoenix.Channel - import GeoTherminatorWeb.Gettext - end - end - - defp view_helpers do - quote do - # Use all HTML functionality (forms, tags, etc) - use Phoenix.HTML - - # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) - import Phoenix.LiveView.Helpers - - # Import basic rendering functionality (render, render_layout, etc) - import Phoenix.View - - alias Phoenix.LiveView.JS - - import GeoTherminatorWeb.ErrorHelpers - import GeoTherminatorWeb.Gettext - alias GeoTherminatorWeb.Router.Helpers, as: Routes - end - end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end -end diff --git a/lib/geo_therminator_web/gettext.ex b/lib/geo_therminator_web/gettext.ex deleted file mode 100644 index 427154d..0000000 --- a/lib/geo_therminator_web/gettext.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule GeoTherminatorWeb.Gettext do - @moduledoc """ - A module providing Internationalization with a gettext-based API. - - By using [Gettext](https://hexdocs.pm/gettext), - your module gains a set of macros for translations, for example: - - import GeoTherminatorWeb.Gettext - - # Simple translation - gettext("Here is the string to translate") - - # Plural translation - ngettext("Here is the string to translate", - "Here are the strings to translate", - 3) - - # Domain-based translation - dgettext("errors", "Here is the error message to translate") - - See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. - """ - use Gettext, otp_app: :geo_therminator -end diff --git a/lib/geo_therminator_web/live/components/main_view.ex b/lib/geo_therminator_web/live/components/main_view.ex deleted file mode 100644 index 474d246..0000000 --- a/lib/geo_therminator_web/live/components/main_view.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule GeoTherminatorWeb.Components.MainView do - use GeoTherminatorWeb, :live_component - - @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 - )} - end -end diff --git a/lib/geo_therminator_web/live/components/main_view.html.heex b/lib/geo_therminator_web/live/components/main_view.html.heex deleted file mode 100644 index 4cca2ba..0000000 --- a/lib/geo_therminator_web/live/components/main_view.html.heex +++ /dev/null @@ -1,508 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - - - <%= @outdoor_temp %>°C - - - - - - - <%= if @priority == :heating do %> - - <% end %> - - <%= if @priority == :hot_water do %> - - <% end %> - - - - - - - <%= @set_temp %>°C - - - - - - - - - - - - - + - - - - - - - - - - - - <%= @brine_in %>°C - - - - - - <%= @brine_out %>°C - - - - - - <%= @supply_in %>°C - - - - - - <%= @supply_out %>°C - - - - - - <%= @hot_water_temp %>°C - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/lib/geo_therminator_web/live/main/device_list.ex b/lib/geo_therminator_web/live/main/device_list.ex deleted file mode 100644 index ee20248..0000000 --- a/lib/geo_therminator_web/live/main/device_list.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule GeoTherminatorWeb.MainLive.DeviceList do - use GeoTherminatorWeb, :live_view - - @impl Phoenix.LiveView - def mount(_params, _session, socket) do - CubDB.delete(GeoTherminator.DB, :viewing_pump) - - user = GeoTherminator.PumpAPI.Auth.Server.get_auth(GeoTherminator.PumpAPI.Auth.Server) - - installations = - GeoTherminator.PumpAPI.Auth.Server.get_installations(GeoTherminator.PumpAPI.Auth.Server) - - {:ok, assign(socket, user: user, installations: installations)} - end - - @impl Phoenix.LiveView - def handle_event(evt, value, socket) - - def handle_event("logout", _value, socket) do - DynamicSupervisor.stop(GeoTherminator.PumpAPI.Auth.Server.Supervisor) - CubDB.delete(GeoTherminator.DB, :auth_credentials) - - {:noreply, - Phoenix.LiveView.push_redirect(socket, - to: Routes.live_path(socket, GeoTherminatorWeb.MainLive.Index) - )} - end -end diff --git a/lib/geo_therminator_web/live/main/device_list.html.heex b/lib/geo_therminator_web/live/main/device_list.html.heex deleted file mode 100644 index 51cba2a..0000000 --- a/lib/geo_therminator_web/live/main/device_list.html.heex +++ /dev/null @@ -1,22 +0,0 @@ -
-
-

Welcome, <%= @user.first_name %>!

-
- -
-

Your available pumps

- -
- - -
diff --git a/lib/geo_therminator_web/live/main/index.ex b/lib/geo_therminator_web/live/main/index.ex deleted file mode 100644 index f8159c7..0000000 --- a/lib/geo_therminator_web/live/main/index.ex +++ /dev/null @@ -1,62 +0,0 @@ -defmodule GeoTherminatorWeb.MainLive.Index do - use GeoTherminatorWeb, :live_view - - @impl Phoenix.LiveView - def mount(_params, _session, socket) do - socket = assign(socket, error: false) - - credentials = CubDB.get(GeoTherminator.DB, :auth_credentials) - - socket = - if connected?(socket) and is_map(credentials) do - try_init(socket, credentials["username"], credentials["password"]) - else - socket - end - - {:ok, socket} - end - - @impl Phoenix.LiveView - def handle_event(evt, val, socket) - - def handle_event("login", values, socket) do - {:noreply, try_init(socket, values["username"], values["password"])} - end - - defp try_init(socket, username, password) do - on_start = - DynamicSupervisor.start_child( - GeoTherminator.PumpAPI.Auth.Server.Supervisor, - {GeoTherminator.PumpAPI.Auth.Server, - %GeoTherminator.PumpAPI.Auth.Server.Options{ - server_name: GeoTherminator.PumpAPI.Auth.Server, - username: username, - password: password - }} - ) - - case on_start do - {:ok, _} -> - CubDB.put(GeoTherminator.DB, :auth_credentials, %{ - "username" => username, - "password" => password - }) - - viewing_pump = CubDB.get(GeoTherminator.DB, :viewing_pump) - - if viewing_pump != nil do - push_redirect(socket, - to: Routes.live_path(socket, GeoTherminatorWeb.MainLive.Pump, viewing_pump) - ) - else - push_redirect(socket, - to: Routes.live_path(socket, GeoTherminatorWeb.MainLive.DeviceList) - ) - end - - _ -> - assign(socket, error: true) - 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 deleted file mode 100644 index 347d5b8..0000000 --- a/lib/geo_therminator_web/live/main/index.html.heex +++ /dev/null @@ -1,17 +0,0 @@ -
- <%= if connected?(@socket) do %> -
-

Log in

- - - - - - <%= if @error do %> -
Error occurred, please try again.
- <% end %> -
- <% else %> -

Loading…

- <% end %> -
diff --git a/lib/geo_therminator_web/live/main/pump.ex b/lib/geo_therminator_web/live/main/pump.ex deleted file mode 100644 index d6e7cad..0000000 --- a/lib/geo_therminator_web/live/main/pump.ex +++ /dev/null @@ -1,94 +0,0 @@ -defmodule GeoTherminatorWeb.MainLive.Pump do - use GeoTherminatorWeb, :live_view - alias GeoTherminator.PumpAPI.Auth - alias GeoTherminator.PumpAPI.Device - require Logger - - @impl true - def mount(%{"id" => str_id}, _session, socket) do - socket = - with {id, ""} <- Integer.parse(str_id), - 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 - CubDB.put(GeoTherminator.DB, :viewing_pump, str_id) - - assign(socket, - pid: pid, - device: device, - status: status, - registers: registers, - opstat: opstat - ) - else - err -> - Logger.debug("EXPLODY #{inspect(err)}") - assign(socket, pid: nil, device: nil, status: nil, registers: nil, opstat: nil) - end - - {:ok, socket} - end - - @impl true - 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 - _ = Device.Server.set_temp(socket.assigns.pid, current + 1) - - optimistic_status = %Device.Status{ - socket.assigns.status - | is_heating_effect_set_by_user: true - } - - {:noreply, assign(socket, status: optimistic_status)} - else - {:noreply, socket} - end - 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 - _ = Device.Server.set_temp(socket.assigns.pid, current - 1) - - optimistic_status = %Device.Status{ - socket.assigns.status - | is_heating_effect_set_by_user: true - } - - {:noreply, assign(socket, status: optimistic_status)} - else - {:noreply, socket} - end - end - - @impl true - def handle_info(msg, socket) - - def handle_info({:device, device}, socket) do - {:noreply, assign(socket, device: device)} - end - - def handle_info({:status, status}, socket) do - {:noreply, assign(socket, status: status)} - end - - def handle_info({:registers, registers}, socket) do - {:noreply, assign(socket, registers: registers)} - end - - def handle_info({:opstat, opstat}, socket) do - {:noreply, assign(socket, opstat: opstat)} - end - - def handle_info(msg, socket) do - Logger.debug("Unknown message: #{inspect(msg)}") - {:noreply, socket} - end -end diff --git a/lib/geo_therminator_web/live/main/pump.html.heex b/lib/geo_therminator_web/live/main/pump.html.heex deleted file mode 100644 index 6a3c995..0000000 --- a/lib/geo_therminator_web/live/main/pump.html.heex +++ /dev/null @@ -1,16 +0,0 @@ -<%= if is_nil(@pid) do %> - Some fail happened. -<% else %> - <%= if is_nil(@device) do %> - Loading pump data… - <% else %> - <.live_component - module={GeoTherminatorWeb.Components.MainView} - id={"pump-#{@device.id}"} - device={@device} - status={@status} - registers={@registers} - opstat={@opstat} - /> - <% end %> -<% end %> diff --git a/lib/geo_therminator_web/router.ex b/lib/geo_therminator_web/router.ex deleted file mode 100644 index b0bf41c..0000000 --- a/lib/geo_therminator_web/router.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule GeoTherminatorWeb.Router do - use GeoTherminatorWeb, :router - - pipeline :browser do - plug :accepts, ["html"] - plug :fetch_session - plug :fetch_live_flash - plug :put_root_layout, {GeoTherminatorWeb.LayoutView, :root} - plug :protect_from_forgery - plug :put_secure_browser_headers - end - - pipeline :api do - plug :accepts, ["json"] - end - - scope "/", GeoTherminatorWeb do - pipe_through :browser - - live "/", MainLive.Index - live "/devices", MainLive.DeviceList - live "/pump/:id", MainLive.Pump - end - - # Other scopes may use custom stacks. - # scope "/api", GeoTherminatorWeb do - # pipe_through :api - # end - - # Enables LiveDashboard only for development - # - # If you want to use the LiveDashboard in production, you should put - # it behind authentication and allow only admins to access it. - # If your application does not have an admins-only section yet, - # you can use Plug.BasicAuth to set up some basic authentication - # as long as you are also using SSL (which you should anyway). - if Mix.env() in [:dev, :test] do - import Phoenix.LiveDashboard.Router - - scope "/" do - pipe_through :browser - live_dashboard "/dashboard", metrics: GeoTherminatorWeb.Telemetry - end - end -end diff --git a/lib/geo_therminator_web/telemetry.ex b/lib/geo_therminator_web/telemetry.ex deleted file mode 100644 index 659425e..0000000 --- a/lib/geo_therminator_web/telemetry.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule GeoTherminatorWeb.Telemetry do - use Supervisor - import Telemetry.Metrics - - def start_link(arg) do - Supervisor.start_link(__MODULE__, arg, name: __MODULE__) - end - - @impl true - def init(_arg) do - children = [ - # Telemetry poller will execute the given period measurements - # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics - {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} - # Add reporters as children of your supervision tree. - # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} - ] - - Supervisor.init(children, strategy: :one_for_one) - end - - def metrics do - [ - # Phoenix Metrics - summary("phoenix.endpoint.stop.duration", - unit: {:native, :millisecond} - ), - summary("phoenix.router_dispatch.stop.duration", - tags: [:route], - unit: {:native, :millisecond} - ), - - # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), - summary("vm.total_run_queue_lengths.total"), - summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io") - ] - end - - defp periodic_measurements do - [ - # A module, function and arguments to be invoked periodically. - # This function must call :telemetry.execute/3 and a metric must be added above. - # {GeoTherminatorWeb, :count_users, []} - ] - end -end diff --git a/lib/geo_therminator_web/templates/layout/app.html.heex b/lib/geo_therminator_web/templates/layout/app.html.heex deleted file mode 100644 index 169aed9..0000000 --- a/lib/geo_therminator_web/templates/layout/app.html.heex +++ /dev/null @@ -1,5 +0,0 @@ -
- - - <%= @inner_content %> -
diff --git a/lib/geo_therminator_web/templates/layout/live.html.heex b/lib/geo_therminator_web/templates/layout/live.html.heex deleted file mode 100644 index a29d604..0000000 --- a/lib/geo_therminator_web/templates/layout/live.html.heex +++ /dev/null @@ -1,11 +0,0 @@ -
- - - - - <%= @inner_content %> -
diff --git a/lib/geo_therminator_web/templates/layout/root.html.heex b/lib/geo_therminator_web/templates/layout/root.html.heex deleted file mode 100644 index b8e2cfc..0000000 --- a/lib/geo_therminator_web/templates/layout/root.html.heex +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - <%= csrf_meta_tag() %> - <%= live_title_tag assigns[:page_title] || "Main", suffix: " · GeoTherminator" %> - - - - - <%= @inner_content %> - - diff --git a/lib/geo_therminator_web/views/error_helpers.ex b/lib/geo_therminator_web/views/error_helpers.ex deleted file mode 100644 index 6eee012..0000000 --- a/lib/geo_therminator_web/views/error_helpers.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule GeoTherminatorWeb.ErrorHelpers do - @moduledoc """ - Conveniences for translating and building error messages. - """ - - use Phoenix.HTML - - @doc """ - Generates tag for inlined form input errors. - """ - def error_tag(form, field) do - Enum.map(Keyword.get_values(form.errors, field), fn error -> - content_tag(:span, translate_error(error), - class: "invalid-feedback", - phx_feedback_for: input_name(form, field) - ) - end) - end - - @doc """ - Translates an error message using gettext. - """ - def translate_error({msg, opts}) do - # When using gettext, we typically pass the strings we want - # to translate as a static argument: - # - # # Translate "is invalid" in the "errors" domain - # dgettext("errors", "is invalid") - # - # # Translate the number of files with plural rules - # dngettext("errors", "1 file", "%{count} files", count) - # - # Because the error messages we show in our forms and APIs - # are defined inside Ecto, we need to translate them dynamically. - # This requires us to call the Gettext module passing our gettext - # backend as first argument. - # - # Note we use the "errors" domain, which means translations - # should be written to the errors.po file. The :count option is - # set by Ecto and indicates we should also apply plural rules. - if count = opts[:count] do - Gettext.dngettext(GeoTherminatorWeb.Gettext, "errors", msg, msg, count, opts) - else - Gettext.dgettext(GeoTherminatorWeb.Gettext, "errors", msg, opts) - end - end -end diff --git a/lib/geo_therminator_web/views/error_view.ex b/lib/geo_therminator_web/views/error_view.ex deleted file mode 100644 index 4bffcab..0000000 --- a/lib/geo_therminator_web/views/error_view.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule GeoTherminatorWeb.ErrorView do - use GeoTherminatorWeb, :view - - # If you want to customize a particular status code - # for a certain format, you may uncomment below. - # def render("500.html", _assigns) do - # "Internal Server Error" - # end - - # By default, Phoenix returns the status message from - # the template name. For example, "404.html" becomes - # "Not Found". - def template_not_found(template, _assigns) do - Phoenix.Controller.status_message_from_template(template) - end -end diff --git a/lib/geo_therminator_web/views/layout_view.ex b/lib/geo_therminator_web/views/layout_view.ex deleted file mode 100644 index 47278a4..0000000 --- a/lib/geo_therminator_web/views/layout_view.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule GeoTherminatorWeb.LayoutView do - use GeoTherminatorWeb, :view - - # Phoenix LiveDashboard is available only in development by default, - # so we instruct Elixir to not warn if the dashboard route is missing. - @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} -end diff --git a/assets/mainview.drawio b/mainview.drawio similarity index 100% rename from assets/mainview.drawio rename to mainview.drawio diff --git a/manifest.toml b/manifest.toml index 6cfed50..08f354d 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,25 +2,17 @@ # You typically do not need to edit this file packages = [ - { name = "certifi", version = "2.9.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641" }, - { name = "gleam_erlang", version = "0.17.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BAAA84F5BCC4477E809BA3E03BB3009A3894A6544C1511626C44408E39DB2AE6" }, - { name = "gleam_hackney", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "hackney", "gleam_http"], otp_app = "gleam_hackney", source = "hex", outer_checksum = "CCACA00027C827436D8EB945651392B6E5798CFC9E69907A28BE61832B0C02A4" }, - { name = "gleam_http", version = "3.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B66B7A1539CCB577119E4DC80DD3484C1A652CB032967954498EEDBAE3355763" }, - { name = "gleam_json", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "E42443C98AA66E30143C24818F2CEA801491C10CE6B1A5EDDF3FC4ABDC7601CB" }, - { name = "gleam_stdlib", version = "0.25.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AD0F89928E0B919C8F8EDF640484633B28DBF88630A9E6AE504617A3E3E5B9A2" }, - { name = "hackney", version = "1.18.1", build_tools = ["rebar3"], requirements = ["certifi", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat", "metrics", "idna"], otp_app = "hackney", source = "hex", outer_checksum = "A4ECDAFF44297E9B5894AE499E9A070EA1888C84AFDD1FD9B7B2BC384950128E" }, - { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, - { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, - { name = "mimerl", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323" }, - { name = "parse_trans", version = "3.3.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B" }, - { name = "ssl_verify_fun", version = "1.1.6", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680" }, - { name = "thoas", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "442296847ACA11DB8D25180693D7CA3073D6D7179F66952F07B16415306513B6" }, - { name = "unicode_util_compat", version = "0.7.0", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521" }, + { name = "gleam_fetch", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "D0C9E9CAE8D6EFCCC3A9FF817DCA9ED327097222086D91DE4F6CA8FBAB02D79F" }, + { name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" }, + { name = "gleam_javascript", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "BFEBB63ABE4A1694E07DEFD19B160C2980304B5D775A89D4B02E7DE7C9D8008B" }, + { name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" }, + { name = "gleam_stdlib", version = "0.30.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "8D8BF3790AA31176B1E1C0B517DD74C86DA8235CF3389EA02043EE4FD82AE3DC" }, + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, ] [requirements] -gleam_erlang = "~> 0.17.1" -gleam_hackney = "~> 0.2.1" -gleam_http = "~> 3.1" -gleam_json = "~> 0.5.0" -gleam_stdlib = "~> 0.25" +gleam_fetch = { version = "~> 0.2.0" } +gleam_http = { version = "~> 3.5" } +gleam_javascript = { version = "~> 0.6.0" } +gleam_json = { version = "~> 0.6.0" } +gleam_stdlib = { version = "~> 0.30" } diff --git a/mix.exs b/mix.exs deleted file mode 100644 index f5cfa3d..0000000 --- a/mix.exs +++ /dev/null @@ -1,97 +0,0 @@ -defmodule GeoTherminator.MixProject do - use Mix.Project - - @app :geo_therminator - - def project do - [ - app: @app, - version: "0.3.0", - elixir: "~> 1.13", - elixirc_paths: elixirc_paths(Mix.env()), - erlc_paths: [ - "build/dev/erlang/#{@app}/_gleam_artefacts" - ], - erlc_include_path: "build/dev/erlang/#{@app}/include", - archives: [mix_gleam: "~> 0.6.1"], - compilers: - if Mix.env() != :test do - [:gleam] - else - [] - end ++ Mix.compilers(), - start_permanent: Mix.env() == :prod, - aliases: aliases(), - deps: deps(), - releases: [ - android: [ - include_executables_for: [:unix], - include_erts: false - ] - ] - ] - end - - # Configuration for the OTP application. - # - # Type `mix help compile.app` for more information. - def application do - [ - mod: {GeoTherminator.Application, []}, - extra_applications: [:logger, :runtime_tools, :logger, :ssl, :crypto, :sasl, :tools, :inets] - ] - end - - # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] - - # Specifies your project dependencies. - # - # Type `mix help deps` for examples and options. - defp deps do - deps_list = [ - {:phoenix, "~> 1.6.2"}, - {:phoenix_html, "~> 3.0"}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:phoenix_live_view, "~> 0.17.11"}, - {:floki, ">= 0.30.0", only: :test}, - {:phoenix_live_dashboard, "~> 0.6"}, - {:esbuild, "~> 0.2", runtime: Mix.env() == :dev}, - {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 1.0"}, - {:gettext, "~> 0.18"}, - {:jason, "~> 1.2"}, - {:plug_cowboy, "~> 2.5"}, - {:dotenv_parser, "~> 1.2"}, - {:finch, "~> 0.9.0"}, - {:desktop, "~> 1.4"}, - {:cubdb, "~> 2.0"}, - {:gleam_stdlib, "~> 0.25"}, - {:gleam_http, "~> 3.1"}, - {:gleam_hackney, "~> 0.2.1"}, - {:gleam_erlang, "~> 0.17.1"}, - {:gleam_json, "~> 0.5.0"} - ] - - if Mix.target() in [:android, :ios] do - deps_list ++ [{:wx, "~> 1.0", hex: :bridge, targets: [:android, :ios]}] - else - deps_list - end - end - - # Aliases are shortcuts or tasks specific to the current project. - # For example, to install project dependencies and perform other setup tasks, run: - # - # $ mix setup - # - # See the documentation for `Mix` for more info on aliases. - defp aliases do - [ - setup: ["deps.get"], - "assets.deploy": ["esbuild default --minify", "phx.digest"], - "deps.get": ["deps.get", "gleam.deps.get"] - ] - end -end diff --git a/mix.lock b/mix.lock deleted file mode 100644 index 2424284..0000000 --- a/mix.lock +++ /dev/null @@ -1,56 +0,0 @@ -%{ - "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, - "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, - "cubdb": {:hex, :cubdb, "2.0.2", "d4253885084dae37a8ff73887d232864eb38ecac962aa08543e686b0183a1d62", [:mix], [], "hexpm", "c99cc8f9e6c4deb98d16cca5ded1928edd22e48b4736b76e8a1a85367d7fe921"}, - "dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"}, - "debouncer": {:hex, :debouncer, "0.1.7", "a7f59fb55cdb54072aff8ece461f4d041d2a709da84e07ed0ab302d348724640", [:mix], [], "hexpm", "b7fd0623df8ab16933bb164d19769884b18c98cab8677cd53eed59587f290603"}, - "desktop": {:hex, :desktop, "1.4.2", "48cb5f02aa77522bd9996bfe02c4b23f8dc40d30076ada46b660f4a20bd7a3a1", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bcaee5daf0c547ed988d26d4bec04388f104ce169a4e255a786cd64598ce3362"}, - "dotenv_parser": {:hex, :dotenv_parser, "1.2.0", "f062900aeb57727b619aeb182fa4a8b1cbb7b4260ebec2b70b3d5c064885aff3", [:mix], [], "hexpm", "eddd69e7fde28618adb2e4153fa380db5c56161b32341e7a4e0530d86987c47f"}, - "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, - "ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"}, - "ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"}, - "expo": {:hex, :expo, "0.1.0", "d4e932bdad052c374118e312e35280f1919ac13881cb3ac07a209a54d0c81dd8", [:mix], [], "hexpm", "c22c536021c56de058aaeedeabb4744eb5d48137bacf8c29f04d25b6c6bbbf45"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "finch": {:hex, :finch, "0.9.1", "ab2b0151ba88543e221cb50bf0734860db55e8748816ee16e4997fe205f7b315", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d6b898a59d19f84958eaffec40580f5a9ff88a31e93156707fa8b1d552aa425"}, - "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, - "gettext": {:hex, :gettext, "0.21.0", "15bbceb20b317b706a8041061a08e858b5a189654128618b53746bf36c84352b", [:mix], [{:expo, "~> 0.1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "04a66db4103b6d1d18f92240bb2c73167b517229316b7bef84e4eebbfb2f14f6"}, - "gleam_erlang": {:hex, :gleam_erlang, "0.17.1", "40fff501e8ca39fa166f4c12ed13bb57e94fc5bb59a93b4446687d82d4a12ff9", [:gleam], [{:gleam_stdlib, "~> 0.22", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}], "hexpm", "baaa84f5bcc4477e809ba3e03bb3009a3894a6544c1511626c44408e39db2ae6"}, - "gleam_hackney": {:hex, :gleam_hackney, "0.2.1", "ca3c5677b85f31885a4366c73a110803515d6d23a2e233e459dc164260315404", [:gleam], [{:gleam_http, "~> 3.0", [hex: :gleam_http, repo: "hexpm", optional: false]}, {:gleam_stdlib, "~> 0.18", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}, {:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "ccaca00027c827436d8eb945651392b6e5798cfc9e69907a28be61832b0c02a4"}, - "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"}, - "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"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, - "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, - "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, - "oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, - "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "1.2.0", "a8ce551485a9a3dac8d523542de130eafd12e40bbf76cf0ecd2528f24e812a44", [:rebar3], [], "hexpm", "1427e73667b9a2002cf1f26694c422d5c905df889023903c4518921d53e3e883"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, - "thoas": {:hex, :thoas, "0.4.0", "86a72ccdc5ec388a13f9f843bcd6c1076640233b95440e47ffb8e3c0dbdb5a17", [:rebar3], [], "hexpm", "442296847aca11db8d25180693d7ca3073d6d7179f66952f07b16415306513b6"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, -} diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po deleted file mode 100644 index cdec3a1..0000000 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ /dev/null @@ -1,11 +0,0 @@ -## `msgid`s in this file come from POT (.pot) files. -## -## Do not add, change, or remove `msgid`s manually here as -## they're tied to the ones in the corresponding POT file -## (with the same domain). -## -## Use `mix gettext.extract --merge` or `mix gettext.merge` -## to merge POT files into PO files. -msgid "" -msgstr "" -"Language: en\n" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot deleted file mode 100644 index d6f47fa..0000000 --- a/priv/gettext/errors.pot +++ /dev/null @@ -1,10 +0,0 @@ -## This is a PO Template file. -## -## `msgid`s here are often extracted from source code. -## Add new translations manually only if they're dynamic -## translations that can't be statically extracted. -## -## Run `mix gettext.extract` to bring this file up to -## date. Leave `msgstr`s empty as changing them here has no -## effect: edit them in PO (`.po`) files instead. - diff --git a/priv/repo/migrations/20211106121919_create_paskas.exs b/priv/repo/migrations/20211106121919_create_paskas.exs deleted file mode 100644 index b9435b7..0000000 --- a/priv/repo/migrations/20211106121919_create_paskas.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule GeoTherminator.Repo.Migrations.CreatePaskas do - use Ecto.Migration - - def change do - create table(:paskas) do - add :name, :string - - timestamps() - end - end -end diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico deleted file mode 100644 index 73de524..0000000 Binary files a/priv/static/favicon.ico and /dev/null differ diff --git a/priv/static/robots.txt b/priv/static/robots.txt deleted file mode 100644 index 3c9c7c0..0000000 --- a/priv/static/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / diff --git a/rel/env.bat.eex b/rel/env.bat.eex deleted file mode 100644 index 60beb80..0000000 --- a/rel/env.bat.eex +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -rem Set the release to work across nodes. -rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". -rem set RELEASE_DISTRIBUTION=name -rem set RELEASE_NODE=<%= @release.name %> diff --git a/rel/env.sh.eex b/rel/env.sh.eex deleted file mode 100644 index 70cfce3..0000000 --- a/rel/env.sh.eex +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# Sets and enables heart (recommended only in daemon mode) -# case $RELEASE_COMMAND in -# daemon*) -# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" -# export HEART_COMMAND -# export ELIXIR_ERL_OPTIONS="-heart" -# ;; -# *) -# ;; -# esac - -# Set the release to work across nodes. -# RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". -# export RELEASE_DISTRIBUTION=name -# export RELEASE_NODE=<%= @release.name %> diff --git a/rel/remote.vm.args.eex b/rel/remote.vm.args.eex deleted file mode 100644 index 5886aa8..0000000 --- a/rel/remote.vm.args.eex +++ /dev/null @@ -1,11 +0,0 @@ -## Customize flags given to the VM: https://erlang.org/doc/man/erl.html -## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here - -## Number of dirty schedulers doing IO work (file, sockets, and others) -##+SDio 5 - -## Increase number of concurrent ports/sockets -##+Q 65536 - -## Tweak GC to run more often -##-env ERL_FULLSWEEP_AFTER 10 diff --git a/rel/vm.args.eex b/rel/vm.args.eex deleted file mode 100644 index 5886aa8..0000000 --- a/rel/vm.args.eex +++ /dev/null @@ -1,11 +0,0 @@ -## Customize flags given to the VM: https://erlang.org/doc/man/erl.html -## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here - -## Number of dirty schedulers doing IO work (file, sockets, and others) -##+SDio 5 - -## Increase number of concurrent ports/sockets -##+Q 65536 - -## Tweak GC to run more often -##-env ERL_FULLSWEEP_AFTER 10 diff --git a/src/azure/b2c.gleam b/src/azure/b2c.gleam deleted file mode 100644 index 5759148..0000000 --- a/src/azure/b2c.gleam +++ /dev/null @@ -1,328 +0,0 @@ -//// Implementation of Azure B2C authentication as used by the Thermia API. -//// 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 - -import gleam/json -import gleam/base -import gleam/hackney -import gleam/uri -import gleam/http -import gleam/http/request -import gleam/http/response -import gleam/dynamic -import gleam/string -import gleam/list -import gleam/result -import gleam/int -import azure/utils -import helpers/crypto -import helpers/uri as uri_helpers -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 { - Tokens( - access_token: String, - access_token_expires_in: Int, - refresh_token: String, - refresh_token_expires_in: Int, - ) -} - -pub type B2CError { - B2CError(msg: String) -} - -type Cookies = - List(#(String, String)) - -type AuthInfo { - AuthInfo(state_code: String, csrf_token: String, cookies: Cookies) -} - -type SelfAsserted { - SelfAsserted(cookies: Cookies) -} - -type Confirmed { - Confirmed(code: String) -} - -/// Authenticate to API with B2C authentication, returning the tokens needed -/// for using the API. -pub fn authenticate( - username: String, - password: String, -) -> Result(Tokens, B2CError) { - let code_challenge = utils.generate_challenge(challenge_length) - try auth_info = authorize(code_challenge) - try self_asserted = signin(username, password, auth_info) - try confirmed = confirm(self_asserted, auth_info) - 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"), - #("code_challenge", hash_challenge(code_challenge)), - #("code_challenge_method", "S256"), - ..base_request_data() - ] - - let req = build_req(authorize_url(), http.Get) - let req = request.set_query(req, auth_data) - - try resp = run_req(req) - - let body_split = string.split(resp.body, "\n") - try settings = - list.find( - body_split, - fn(line) { string.starts_with(line, b2c_authorize_prefix) }, - ) - |> b2c_error("Authorize settings string not found.") - - let prefix_len = string.length(b2c_authorize_prefix) - let settings_json = - string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2) - try data = - json.decode(settings_json, using: dynamic.dynamic) - |> b2c_error( - "Authorize settings JSON parsing error: " <> string.inspect(settings_json), - ) - - try csrf_token = data_get(data, "csrf", dynamic.string) - - try state_code_block = data_get(data, "transId", dynamic.string) - try state_code = - state_code_block - |> string.split("=") - |> list.at(1) - |> b2c_error("State code parsing error: " <> state_code_block) - - Ok(AuthInfo( - state_code: state_code, - csrf_token: csrf_token, - cookies: response.get_cookies(resp), - )) -} - -fn signin( - username: String, - password: String, - auth_info: AuthInfo, -) -> Result(SelfAsserted, B2CError) { - let self_asserted_data = [ - #("request_type", "RESPONSE"), - #("signInName", username), - #("password", password), - ] - - let req = - build_req(self_asserted_url(), http.Post) - |> request.set_body(uri_helpers.form_urlencoded_serialize( - self_asserted_data, - )) - |> request.set_query(base_query(auth_info)) - |> request.set_header("x-csrf-token", auth_info.csrf_token) - - let req = - list.fold( - auth_info.cookies, - req, - fn(acc, cookie) { - let #(name, value) = cookie - request.set_cookie(acc, name, value) - }, - ) - - try resp = run_req(req) - - case string.contains(resp.body, "{\"status\":\"400\"") { - True -> Error(B2CError(msg: "Wrong credentials.")) - False -> Ok(SelfAsserted(cookies: response.get_cookies(resp))) - } -} - -fn confirm( - self_asserted: SelfAsserted, - auth_info: AuthInfo, -) -> Result(Confirmed, B2CError) { - let csrf_cookie_key = "x-ms-cpim-csrf" - - try csrf_cookie = - list.key_find(auth_info.cookies, csrf_cookie_key) - |> b2c_error("CSRF cookie not found in auth info.") - let cookies = [#(csrf_cookie_key, csrf_cookie), ..self_asserted.cookies] - - let req = build_req(confirm_url(), http.Get) - - let req = - list.fold( - cookies, - req, - fn(acc, cookie) { request.set_cookie(acc, cookie.0, cookie.1) }, - ) - |> request.set_query([ - #("csrf_token", auth_info.csrf_token), - ..base_query(auth_info) - ]) - - try resp = - req - |> hackney.send() - |> b2c_error("Confirm HTTP request failed.") - - try resp = case resp.status { - 302 -> Ok(resp) - _ -> Error(B2CError(msg: "Confirm HTTP request bad error code.")) - } - - try location = - response.get_header(resp, "location") - |> b2c_error("Location not found for confirm response.") - - try code = - location - |> string.split("code=") - |> list.at(1) - |> b2c_error("Confirmation code not found.") - - Ok(Confirmed(code: code)) -} - -fn get_tokens( - confirmed: Confirmed, - code_challenge: String, -) -> Result(Tokens, B2CError) { - let request_token_data = [ - #("code", confirmed.code), - #("code_verifier", code_challenge), - #("grant_type", "authorization_code"), - ..base_request_data() - ] - - let req = - build_req(get_token_url(), http.Post) - |> request.set_body(uri_helpers.form_urlencoded_serialize( - request_token_data, - )) - - try resp = run_req(req) - try data = - json.decode(resp.body, using: dynamic.dynamic) - |> b2c_error("Get tokens JSON parsing error: " <> string.inspect(resp.body)) - - try token = data_get(data, "access_token", dynamic.string) - try expires_in = data_get(data, "expires_in", dynamic.int) - try refresh_token = data_get(data, "refresh_token", dynamic.string) - try refresh_token_expires_in = - data_get(data, "refresh_token_expires_in", dynamic.int) - - Ok(Tokens( - access_token: token, - access_token_expires_in: expires_in, - refresh_token: refresh_token, - refresh_token_expires_in: refresh_token_expires_in, - )) -} - -fn hash_challenge(challenge: String) -> String { - let hashed = crypto.hash(crypto.Sha256, challenge) - base.url_encode64(hashed, False) -} - -fn authorize_url() -> String { - b2c_auth_url <> "/oauth2/v2.0/authorize" -} - -fn self_asserted_url() -> String { - b2c_auth_url <> "/SelfAsserted" -} - -fn confirm_url() -> String { - b2c_auth_url <> "/api/CombinedSigninAndSignup/confirmed" -} - -fn get_token_url() -> String { - b2c_auth_url <> "/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) - - let req = request.set_method(req, method) - - case method { - http.Post -> - request.set_header( - req, - "content-type", - "application/x-www-form-urlencoded; charset=UTF-8", - ) - _ -> req - } -} - -fn run_req( - req: request.Request(String), -) -> Result(response.Response(String), B2CError) { - try resp = - req - |> hackney.send() - |> b2c_error("HTTP request failed.") - - case resp.status { - 200 -> Ok(resp) - code -> - Error(B2CError( - msg: "Not OK response code " <> int.to_string(code) <> " from URL " <> uri.to_string(request.to_uri( - req, - )) <> "\n\n" <> string.inspect(resp), - )) - } -} - -fn base_request_data() -> List(#(String, String)) { - [ - #("client_id", b2c_client_id), - #("scope", b2c_scope), - #("redirect_uri", b2c_redirect_uri), - ] -} - -fn base_query(auth_info: AuthInfo) -> List(#(String, String)) { - [ - #("tx", "StateProperties=" <> auth_info.state_code), - #("p", "B2C_1A_SignUpOrSigninOnline"), - ] -} - -fn data_get( - data: dynamic.Dynamic, - key: String, - data_type: dynamic.Decoder(a), -) -> Result(a, B2CError) { - parsing.data_get(data, key, data_type, B2CError) -} - -fn b2c_error(r: Result(a, b), msg: String) -> Result(a, B2CError) { - result.replace_error(r, B2CError(msg)) -} diff --git a/src/config_ffi.mjs b/src/config_ffi.mjs new file mode 100644 index 0000000..cd376db --- /dev/null +++ b/src/config_ffi.mjs @@ -0,0 +1,3 @@ +export function get(key) { + return globalThis.__geo_therminator_config?.[key]; +} diff --git a/src/crypto_ffi.mjs b/src/crypto_ffi.mjs new file mode 100644 index 0000000..ce3b1ba --- /dev/null +++ b/src/crypto_ffi.mjs @@ -0,0 +1,21 @@ +import { Ok, Error, BitString } from "./gleam.mjs"; + +let crypto; + +export async function hash(algo, data) { + if (!crypto) { + if (globalThis.crypto) { + crypto = globalThis.crypto; + } else { + crypto = await import("node:crypto"); + } + } + + try { + const hash = await crypto.subtle.digest(algo, data); + const hashArray = new Uint8Array(hash); + return new Ok(new BitString(hashArray)); + } catch (e) { + return new Error(e); + } +} diff --git a/src/date_ffi.mjs b/src/date_ffi.mjs new file mode 100644 index 0000000..cb68f26 --- /dev/null +++ b/src/date_ffi.mjs @@ -0,0 +1,14 @@ +import { Ok, Error } from "./gleam.mjs"; + +function construct(input) { + const d = new Date(input); + + if (isNaN(d.valueOf())) { + return new Error(undefined); + } else { + return new Ok(d); + } +} + +export const from_iso8601 = construct; +export const from_unix = construct; diff --git a/src/fetch_ffi.mjs b/src/fetch_ffi.mjs new file mode 100644 index 0000000..6088fbc --- /dev/null +++ b/src/fetch_ffi.mjs @@ -0,0 +1,5 @@ +export function preventRedirection(request) { + return new Request(request, { + redirect: "manual", + }); +} diff --git a/src/geo_t/azure/b2c.gleam b/src/geo_t/azure/b2c.gleam new file mode 100644 index 0000000..dd4ad44 --- /dev/null +++ b/src/geo_t/azure/b2c.gleam @@ -0,0 +1,382 @@ +//// Implementation of Azure B2C authentication as used by the Thermia API. +//// 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 + +import gleam/json +import gleam/base +import gleam/uri +import gleam/http +import gleam/http/request +import gleam/http/response +import gleam/dynamic +import gleam/string +import gleam/list +import gleam/result +import gleam/int +import gleam/fetch +import gleam/javascript/promise.{Promise} +import geo_t/azure/utils +import geo_t/helpers/crypto +import geo_t/helpers/uri as uri_helpers +import geo_t/helpers/parsing +import geo_t/helpers/promise as promise_helpers +import geo_t/helpers/fetch as fetch_helpers +import geo_t/config.{Config} + +const challenge_length = 43 + +const b2c_authorize_prefix = "var SETTINGS = " + +pub type Tokens { + Tokens( + access_token: String, + access_token_expires_in: Int, + refresh_token: String, + refresh_token_expires_in: Int, + ) +} + +pub type B2CError { + HashError(inner: crypto.HashingError) + RequestError(inner: fetch.FetchError) + ContentError(msg: String) +} + +type Cookies = + List(#(String, String)) + +type AuthInfo { + AuthInfo(state_code: String, csrf_token: String, cookies: Cookies) +} + +type SelfAsserted { + SelfAsserted(cookies: Cookies) +} + +type Confirmed { + Confirmed(code: String) +} + +/// Authenticate to API with B2C authentication, returning the tokens needed +/// for using the API. +pub fn authenticate( + config: Config, + username: String, + password: String, +) -> Promise(Result(Tokens, B2CError)) { + let code_challenge = utils.generate_challenge(challenge_length) + use auth_info <- promise.try_await(authorize(config, code_challenge)) + use self_asserted <- promise.try_await(signin( + config, + username, + password, + auth_info, + )) + use confirmed <- promise.try_await(confirm(config, self_asserted, auth_info)) + get_tokens(config, confirmed, code_challenge) +} + +fn authorize( + config: Config, + code_challenge: String, +) -> Promise(Result(AuthInfo, B2CError)) { + use challenge <- promise.try_await( + hash_challenge(code_challenge) + |> promise_helpers.map_error(HashError), + ) + + let auth_data = [ + #("response_type", "code"), + #("code_challenge", challenge), + #("code_challenge_method", "S256"), + ..base_request_data(config) + ] + + let req = build_req(authorize_url(config), http.Get) + let req = request.set_query(req, auth_data) + + use resp <- promise.try_await(run_req(req)) + + let body_split = string.split(resp.body, "\n") + use settings <- promise.map_try(promise.resolve( + list.find( + body_split, + fn(line) { string.starts_with(line, b2c_authorize_prefix) }, + ) + |> b2c_error("Authorize settings string not found."), + )) + + let prefix_len = string.length(b2c_authorize_prefix) + let settings_json = + string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2) + use data <- result.try( + json.decode(settings_json, using: dynamic.dynamic) + |> b2c_error( + "Authorize settings JSON parsing error: " <> string.inspect(settings_json), + ), + ) + + use csrf_token <- result.try(data_get(data, "csrf", dynamic.string)) + + use state_code_block <- result.try(data_get(data, "transId", dynamic.string)) + use state_code <- result.try( + state_code_block + |> string.split("=") + |> list.at(1) + |> b2c_error("State code parsing error: " <> state_code_block), + ) + + Ok(AuthInfo( + state_code: state_code, + csrf_token: csrf_token, + cookies: response.get_cookies(resp), + )) +} + +fn signin( + config: Config, + username: String, + password: String, + auth_info: AuthInfo, +) -> Promise(Result(SelfAsserted, B2CError)) { + let self_asserted_data = [ + #("request_type", "RESPONSE"), + #("signInName", username), + #("password", password), + ] + + let req = + build_req(self_asserted_url(config), http.Post) + |> request.set_body(uri_helpers.form_urlencoded_serialize( + self_asserted_data, + )) + |> request.set_query(base_query(auth_info)) + |> request.set_header("x-csrf-token", auth_info.csrf_token) + + let req = + list.fold( + auth_info.cookies, + req, + fn(acc, cookie) { + let #(name, value) = cookie + request.set_cookie(acc, name, value) + }, + ) + + use resp <- promise.try_await(run_req(req)) + + case string.contains(resp.body, "{\"status\":\"400\"") { + True -> promise.resolve(Error(ContentError(msg: "Wrong credentials."))) + False -> + promise.resolve(Ok(SelfAsserted(cookies: response.get_cookies(resp)))) + } +} + +fn confirm( + config: Config, + self_asserted: SelfAsserted, + auth_info: AuthInfo, +) -> Promise(Result(Confirmed, B2CError)) { + let csrf_cookie_key = "x-ms-cpim-csrf" + + use csrf_cookie <- promise.try_await(promise.resolve( + list.key_find(auth_info.cookies, csrf_cookie_key) + |> b2c_error("CSRF cookie not found in auth info."), + )) + let cookies = [#(csrf_cookie_key, csrf_cookie), ..self_asserted.cookies] + + let req = build_req(confirm_url(config), http.Get) + + let req = + list.fold( + cookies, + req, + fn(acc, cookie) { request.set_cookie(acc, cookie.0, cookie.1) }, + ) + |> request.set_query([ + #("csrf_token", auth_info.csrf_token), + ..base_query(auth_info) + ]) + + use resp <- promise.try_await( + req + |> fetch.to_fetch_request() + |> fetch_helpers.prevent_redirection() + |> fetch_helpers.log_raw_send() + |> request_error(), + ) + + use resp <- promise.try_await(case resp.status { + 302 -> promise.resolve(Ok(resp)) + error -> + promise.resolve(Error(ContentError( + msg: "Confirm HTTP request bad error code: " <> int.to_string(error), + ))) + }) + + use location <- promise.try_await(promise.resolve( + response.get_header(resp, "location") + |> b2c_error("Location not found for confirm response."), + )) + + use code <- promise.try_await(promise.resolve( + location + |> string.split("code=") + |> list.at(1) + |> b2c_error("Confirmation code not found."), + )) + + promise.resolve(Ok(Confirmed(code: code))) +} + +fn get_tokens( + config: Config, + confirmed: Confirmed, + code_challenge: String, +) -> Promise(Result(Tokens, B2CError)) { + let request_token_data = [ + #("code", confirmed.code), + #("code_verifier", code_challenge), + #("grant_type", "authorization_code"), + ..base_request_data(config) + ] + + let req = + build_req(get_token_url(config), http.Post) + |> request.set_body(uri_helpers.form_urlencoded_serialize( + request_token_data, + )) + + use resp <- promise.try_await(run_req(req)) + + use data <- promise.try_await(promise.resolve( + json.decode(resp.body, using: dynamic.dynamic) + |> b2c_error("Get tokens JSON parsing error: " <> string.inspect(resp.body)), + )) + + use token <- promise.try_await(promise.resolve(data_get( + data, + "access_token", + dynamic.string, + ))) + use expires_in <- promise.try_await(promise.resolve(data_get( + data, + "expires_in", + dynamic.int, + ))) + use refresh_token <- promise.try_await(promise.resolve(data_get( + data, + "refresh_token", + dynamic.string, + ))) + use refresh_token_expires_in <- promise.try_await(promise.resolve(data_get( + data, + "refresh_token_expires_in", + dynamic.int, + ))) + + promise.resolve(Ok(Tokens( + access_token: token, + access_token_expires_in: expires_in, + refresh_token: refresh_token, + refresh_token_expires_in: refresh_token_expires_in, + ))) +} + +fn hash_challenge( + challenge: String, +) -> Promise(Result(String, crypto.HashingError)) { + use hashed <- promise.try_await(crypto.hash(crypto.Sha256, challenge)) + promise.resolve(Ok(base.url_encode64(hashed, False))) +} + +fn authorize_url(config: Config) -> uri.Uri { + resolve_url(config.b2c_auth_url, "/oauth2/v2.0/authorize") +} + +fn self_asserted_url(config: Config) -> uri.Uri { + resolve_url(config.b2c_auth_url, "/SelfAsserted") +} + +fn confirm_url(config: Config) -> uri.Uri { + resolve_url(config.b2c_auth_url, "/api/CombinedSigninAndSignup/confirmed") +} + +fn get_token_url(config: Config) -> uri.Uri { + resolve_url(config.b2c_auth_url, "/oauth2/v2.0/token") +} + +fn resolve_url(base: uri.Uri, relative: String) -> uri.Uri { + let assert Ok(relative) = uri.parse(relative) + uri.Uri(..base, path: base.path <> relative.path) +} + +fn build_req(url: uri.Uri, method: http.Method) -> request.Request(String) { + let assert Ok(req) = request.from_uri(url) + + let req = request.set_method(req, method) + + case method { + http.Post -> + request.set_header( + req, + "content-type", + "application/x-www-form-urlencoded; charset=UTF-8", + ) + _ -> req + } +} + +fn run_req( + req: request.Request(String), +) -> Promise(Result(response.Response(String), B2CError)) { + use resp <- promise.try_await( + req + |> fetch_helpers.log_send() + |> request_error(), + ) + + case resp.status { + 200 -> promise.resolve(Ok(resp)) + code -> + promise.resolve(Error(ContentError( + msg: "Not OK response code " <> int.to_string(code) <> " from URL " <> uri.to_string(request.to_uri( + req, + )) <> "\n\n" <> string.inspect(resp), + ))) + } +} + +fn base_request_data(config: Config) -> List(#(String, String)) { + [ + #("client_id", config.b2c_client_id), + #("scope", config.b2c_client_id), + #("redirect_uri", uri.to_string(config.b2c_redirect_url)), + ] +} + +fn base_query(auth_info: AuthInfo) -> List(#(String, String)) { + [ + #("tx", "StateProperties=" <> auth_info.state_code), + #("p", "B2C_1A_SignUpOrSigninOnline"), + ] +} + +fn data_get( + data: dynamic.Dynamic, + key: String, + data_type: dynamic.Decoder(a), +) -> Result(a, B2CError) { + parsing.data_get(data, key, data_type, ContentError) +} + +fn b2c_error(r: Result(a, b), msg: String) -> Result(a, B2CError) { + result.replace_error(r, ContentError(msg)) +} + +fn request_error( + p: Promise(Result(a, fetch.FetchError)), +) -> Promise(Result(a, B2CError)) { + promise_helpers.map_error(p, RequestError) +} diff --git a/src/azure/utils.gleam b/src/geo_t/azure/utils.gleam similarity index 51% rename from src/azure/utils.gleam rename to src/geo_t/azure/utils.gleam index 8951862..3d029bd 100644 --- a/src/azure/utils.gleam +++ b/src/geo_t/azure/utils.gleam @@ -1,28 +1,19 @@ import gleam/string import gleam/list import gleam/int -import gleam/bit_string -import helpers/binary const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const alphabet_len = 62 pub fn generate_challenge(length: Int) -> String { - let alphabet_str = bit_string.from_string(alphabet) - list.range(0, length - 1) - |> list.map(fn(_) { random_char(alphabet_str) }) + |> list.map(fn(_) { random_char(alphabet) }) |> string.join(with: "") } -fn random_char(alphabet: BitString) -> String { +fn random_char(alphabet: String) -> String { let index = int.random(0, alphabet_len) - assert Ok(char) = - alphabet - |> binary.part(index, 1) - |> bit_string.to_string() - - char + string.slice(alphabet, index, 1) } diff --git a/src/geo_t/config.gleam b/src/geo_t/config.gleam new file mode 100644 index 0000000..104b8df --- /dev/null +++ b/src/geo_t/config.gleam @@ -0,0 +1,117 @@ +import gleam/map.{Map} +import gleam/uri.{Uri} +import gleam/result +import geo_t/pump_api/device/opstat.{ + ActiveCooling, AntiLegionella, Defrost, HandOperated, Heating, HotWater, Idle, + Off, OpStat, PassiveCooling, Pool, Standby, +} +import geo_t/helpers/config as config_helpers + +pub type Config { + Config( + debug: Bool, + api_timeout: Int, + api_auth_url: Uri, + api_installations_url: Uri, + api_device_url: Uri, + api_device_status_url: Uri, + api_device_register_url: Uri, + api_device_opstat_url: Uri, + api_device_reg_set_url: Uri, + api_device_temp_set_reg_index: Int, + api_device_reg_set_client_id: String, + api_refresh: Int, + b2c_client_id: String, + b2c_redirect_url: Uri, + b2c_auth_url: Uri, + api_opstat_mapping: Map(Int, OpStat), + api_opstat_bitmask_mapping: Map(Int, OpStat), + ) +} + +pub fn load_config() -> Config { + Config( + debug: get_env(config_helpers.debug, False), + api_timeout: get_env(config_helpers.api_timeout, 30_000), + api_auth_url: get_uri( + config_helpers.api_auth_url, + "https://thermia-auth-api.azurewebsites.net/api/v1/Jwt/login", + ), + api_installations_url: get_uri( + config_helpers.api_installations_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installationsInfo", + ), + api_device_url: get_uri( + config_helpers.api_device_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installations/{id}", + ), + api_device_status_url: get_uri( + config_helpers.api_device_status_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/installationstatus/{id}/status", + ), + api_device_register_url: get_uri( + config_helpers.api_device_register_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Groups/REG_GROUP_TEMPERATURES", + ), + api_device_opstat_url: get_uri( + config_helpers.api_device_opstat_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Groups/REG_GROUP_OPERATIONAL_STATUS", + ), + api_opstat_mapping: map.from_list([ + #(1, HandOperated), + #(3, HotWater), + #(4, Heating), + #(5, ActiveCooling), + #(6, Pool), + #(7, AntiLegionella), + #(8, PassiveCooling), + #(98, Standby), + #(99, Idle), + #(100, Off), + ]), + api_opstat_bitmask_mapping: map.from_list([ + #(1, HandOperated), + #(2, Defrost), + #(4, HotWater), + #(8, Heating), + #(16, ActiveCooling), + #(32, Pool), + #(64, AntiLegionella), + #(128, PassiveCooling), + #(512, Standby), + #(1024, Idle), + #(2048, Off), + ]), + api_device_reg_set_url: get_uri( + config_helpers.api_device_reg_set_url, + "https://online-genesis-serviceapi.azurewebsites.net/api/v1/Registers/Installations/{id}/Registers", + ), + api_device_temp_set_reg_index: 3, + api_device_reg_set_client_id: get_env( + config_helpers.api_device_reg_set_client_id, + "", + ), + api_refresh: 10_000, + b2c_client_id: get_env( + config_helpers.b2c_client_id, + "09ea4903-9e95-45fe-ae1f-e3b7d32fa385", + ), + b2c_redirect_url: get_uri( + config_helpers.b2c_redirect_url, + "https://online-genesis.thermia.se/login", + ), + b2c_auth_url: get_uri( + config_helpers.b2c_auth_url, + "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft.com/b2c_1a_signuporsigninonline", + ), + ) +} + +fn get_env(getter: fn() -> Result(a, b), default: a) -> a { + result.unwrap(getter(), default) +} + +fn get_uri(getter: fn() -> Result(Uri, b), default: String) -> Uri { + let assert Ok(default) = uri.parse(default) + get_env(getter, default) +} diff --git a/src/geo_t/helpers/config.gleam b/src/geo_t/helpers/config.gleam new file mode 100644 index 0000000..71cf28c --- /dev/null +++ b/src/geo_t/helpers/config.gleam @@ -0,0 +1,74 @@ +import gleam/dynamic +import gleam/uri +import geo_t/helpers/try.{try} + +pub const app_name = "geo_therminator" + +pub fn debug() { + config("debug", dynamic.bool) +} + +pub fn api_timeout() { + config("api_timeout", dynamic.int) +} + +pub fn api_auth_url() { + config_url("api_auth_url") +} + +pub fn api_installations_url() { + config_url("api_installations_url") +} + +pub fn api_device_url() { + config_url("api_device_url") +} + +pub fn api_device_status_url() { + config_url("api_device_status_url") +} + +pub fn api_device_register_url() { + config_url("api_device_register_url") +} + +pub fn api_device_opstat_url() { + config_url("api_device_opstat_url") +} + +pub fn api_device_reg_set_url() { + config_url("api_device_reg_set_url") +} + +pub fn b2c_redirect_url() { + config_url("b2c_redirect_url") +} + +pub fn b2c_auth_url() { + config_url("b2c_auth_url") +} + +pub fn b2c_client_id() { + config("b2c_client_id", dynamic.string) +} + +pub fn api_device_reg_set_client_id() { + config("api_device_reg_set_client_id", dynamic.string) +} + +fn config(config_key: String, decoder: dynamic.Decoder(a)) { + config_key + |> get_conf() + |> decoder() +} + +fn config_url(config_key: String) -> Result(uri.Uri, Nil) { + let url = get_conf(config_key) + + use url_str <- try(dynamic.string(url), fn(_) { Nil }) + use parsed_url <- try(uri.parse(url_str), fn(_) { Nil }) + Ok(parsed_url) +} + +@external(javascript, "../../config_ffi.mjs", "get") +fn get_conf(key: String) -> dynamic.Dynamic diff --git a/src/geo_t/helpers/crypto.gleam b/src/geo_t/helpers/crypto.gleam new file mode 100644 index 0000000..da60788 --- /dev/null +++ b/src/geo_t/helpers/crypto.gleam @@ -0,0 +1,27 @@ +import gleam/javascript/promise.{Promise} + +pub type HashAlgorithm { + // For now, only SHA256 + Sha256 +} + +pub type HashingError + +pub fn hash( + algo: HashAlgorithm, + data: String, +) -> Promise(Result(BitString, HashingError)) { + do_hash(algo_to_string(algo), data) +} + +fn algo_to_string(algo: HashAlgorithm) -> String { + case algo { + Sha256 -> "SHA-256" + } +} + +@external(javascript, "../../crypto_ffi.mjs", "hash") +fn do_hash( + algo algo: String, + data data: String, +) -> Promise(Result(BitString, HashingError)) diff --git a/src/geo_t/helpers/date.gleam b/src/geo_t/helpers/date.gleam new file mode 100644 index 0000000..b2f18b6 --- /dev/null +++ b/src/geo_t/helpers/date.gleam @@ -0,0 +1,7 @@ +pub type Date + +@external(javascript, "../../date_ffi.mjs", "from_iso8601") +pub fn from_iso8601(a: String) -> Result(Date, Nil) + +@external(javascript, "../../date_ffi.mjs", "from_unix") +pub fn from_unix(a: Int) -> Result(Date, Nil) diff --git a/src/geo_t/helpers/debug.gleam b/src/geo_t/helpers/debug.gleam new file mode 100644 index 0000000..74d6e02 --- /dev/null +++ b/src/geo_t/helpers/debug.gleam @@ -0,0 +1,12 @@ +import geo_t/config.{Config} + +pub fn if_debug(config: Config, do: fn() -> a) { + case config.debug { + True -> { + do() + Nil + } + + False -> Nil + } +} diff --git a/src/geo_t/helpers/fetch.gleam b/src/geo_t/helpers/fetch.gleam new file mode 100644 index 0000000..5d29dbe --- /dev/null +++ b/src/geo_t/helpers/fetch.gleam @@ -0,0 +1,30 @@ +import gleam/io +import gleam/http/request.{Request} +import gleam/javascript/promise +import gleam/string +import gleam/fetch as og_fetch + +const debug_print_len = 1000 + +pub fn log_send(req: Request(String)) { + io.println("Sending request:") + io.debug(req) + use resp <- promise.try_await(og_fetch.send(req)) + use resp <- promise.try_await(og_fetch.read_text_body(resp)) + io.println(string.slice(string.inspect(resp), 0, debug_print_len)) + promise.resolve(Ok(resp)) +} + +pub fn log_raw_send(req: og_fetch.FetchRequest) { + io.println("Sending raw request:") + io.debug(req) + use resp <- promise.try_await(og_fetch.raw_send(req)) + use resp <- promise.try_await(og_fetch.read_text_body(og_fetch.from_fetch_response( + resp, + ))) + io.println(string.slice(string.inspect(resp), 0, debug_print_len)) + promise.resolve(Ok(resp)) +} + +@external(javascript, "../../fetch_ffi.mjs", "preventRedirection") +pub fn prevent_redirection(req: og_fetch.FetchRequest) -> og_fetch.FetchRequest diff --git a/src/helpers/parsing.gleam b/src/geo_t/helpers/parsing.gleam similarity index 100% rename from src/helpers/parsing.gleam rename to src/geo_t/helpers/parsing.gleam diff --git a/src/geo_t/helpers/promise.gleam b/src/geo_t/helpers/promise.gleam new file mode 100644 index 0000000..dcd0db4 --- /dev/null +++ b/src/geo_t/helpers/promise.gleam @@ -0,0 +1,13 @@ +import gleam/result +import gleam/javascript/promise.{Promise} as og_promise + +pub fn map_error( + p: Promise(Result(a, b)), + mapper: fn(b) -> c, +) -> Promise(Result(a, c)) { + og_promise.map(p, fn(r) { result.map_error(r, mapper) }) +} + +pub fn replace_error(p: Promise(Result(a, b)), err: c) -> Promise(Result(a, c)) { + og_promise.map(p, fn(r) { result.replace_error(r, err) }) +} diff --git a/src/geo_t/helpers/try.gleam b/src/geo_t/helpers/try.gleam new file mode 100644 index 0000000..5d03dd6 --- /dev/null +++ b/src/geo_t/helpers/try.gleam @@ -0,0 +1,13 @@ +import gleam/result + +pub fn try( + val: Result(val_a, err_a), + err_mapper: fn(err_a) -> err_b, + callback: fn(val_a) -> Result(val_b, err_b), +) { + result.try( + val + |> result.map_error(err_mapper), + callback, + ) +} diff --git a/src/helpers/uri.gleam b/src/geo_t/helpers/uri.gleam similarity index 100% rename from src/helpers/uri.gleam rename to src/geo_t/helpers/uri.gleam diff --git a/src/geo_t/pump_api/auth/api.gleam b/src/geo_t/pump_api/auth/api.gleam new file mode 100644 index 0000000..523e26b --- /dev/null +++ b/src/geo_t/pump_api/auth/api.gleam @@ -0,0 +1,99 @@ +import gleam/http/request +import gleam/json +import gleam/result +import gleam/dynamic +import gleam/list +import gleam/string +import gleam/javascript/promise.{Promise} +import geo_t/pump_api/auth/user.{User} +import geo_t/pump_api/auth/tokens.{Tokens} +import geo_t/pump_api/auth/installation_info.{InstallationInfo} +import geo_t/pump_api/http +import geo_t/helpers/date +import geo_t/helpers/parsing +import geo_t/azure/b2c.{B2CError} +import geo_t/helpers/promise as promise_helpers +import geo_t/helpers/fetch as fetch_helpers +import geo_t/config.{Config} + +pub type ApiError { + ApiRequestFailed + NotOkResponse + InvalidData(msg: String) + AuthError(inner: B2CError) +} + +pub fn auth( + config: Config, + username: String, + password: String, +) -> Promise(Result(User, ApiError)) { + use tokens <- promise.try_await( + b2c.authenticate(config, username, password) + |> promise_helpers.map_error(AuthError), + ) + use access_token_expires_in <- promise.try_await(promise.resolve( + date.from_unix(tokens.access_token_expires_in) + |> result.replace_error(InvalidData( + msg: "Access token expiry could not be converted into DateTime: " <> string.inspect( + tokens.access_token_expires_in, + ), + )), + )) + use refresh_token_expires_in <- promise.try_await(promise.resolve( + date.from_unix(tokens.refresh_token_expires_in) + |> result.replace_error(InvalidData( + msg: "Refresh token expiry could not be converted into DateTime: " <> string.inspect( + tokens.refresh_token_expires_in, + ), + )), + )) + + promise.resolve(Ok(User(tokens: Tokens( + access_token: tokens.access_token, + access_token_expiry: access_token_expires_in, + refresh_token: tokens.refresh_token, + refresh_token_expiry: refresh_token_expires_in, + )))) +} + +pub fn installation_info( + config: Config, + user: User, +) -> Promise(Result(List(InstallationInfo), ApiError)) { + let url = config.api_installations_url + let assert Ok(raw_req) = request.from_uri(url) + + let empty_req = request.set_body(raw_req, http.Empty) + use data <- promise.try_await(run_req(http.authed_req(user, empty_req))) + + use items <- promise.try_await(promise.resolve(parsing.data_get( + data, + "items", + dynamic.list(of: dynamic.field("id", dynamic.int)), + InvalidData, + ))) + + promise.resolve(Ok(list.map(items, fn(id) { InstallationInfo(id: id) }))) +} + +fn run_req(req: request.Request(String)) { + use resp <- promise.try_await( + req + |> fetch_helpers.log_send() + |> promise_helpers.replace_error(ApiRequestFailed), + ) + + use body <- promise.try_await(promise.resolve(case resp.status { + 200 -> Ok(resp.body) + _ -> Error(NotOkResponse) + })) + + promise.resolve( + body + |> json.decode(using: dynamic.dynamic) + |> result.replace_error(InvalidData( + msg: "Could not parse InstallationInfo JSON.", + )), + ) +} diff --git a/src/pump_api/auth/installation_info.gleam b/src/geo_t/pump_api/auth/installation_info.gleam similarity index 100% rename from src/pump_api/auth/installation_info.gleam rename to src/geo_t/pump_api/auth/installation_info.gleam diff --git a/src/geo_t/pump_api/auth/tokens.gleam b/src/geo_t/pump_api/auth/tokens.gleam new file mode 100644 index 0000000..224a322 --- /dev/null +++ b/src/geo_t/pump_api/auth/tokens.gleam @@ -0,0 +1,10 @@ +import geo_t/helpers/date.{Date} + +pub type Tokens { + Tokens( + access_token: String, + access_token_expiry: Date, + refresh_token: String, + refresh_token_expiry: Date, + ) +} diff --git a/src/pump_api/auth/user.gleam b/src/geo_t/pump_api/auth/user.gleam similarity index 59% rename from src/pump_api/auth/user.gleam rename to src/geo_t/pump_api/auth/user.gleam index 7e40b64..7e50050 100644 --- a/src/pump_api/auth/user.gleam +++ b/src/geo_t/pump_api/auth/user.gleam @@ -1,4 +1,4 @@ -import pump_api/auth/tokens +import geo_t/pump_api/auth/tokens pub type User { User(tokens: tokens.Tokens) diff --git a/src/geo_t/pump_api/device/opstat.gleam b/src/geo_t/pump_api/device/opstat.gleam new file mode 100644 index 0000000..ae71723 --- /dev/null +++ b/src/geo_t/pump_api/device/opstat.gleam @@ -0,0 +1,13 @@ +pub type OpStat { + HandOperated + Defrost + HotWater + Heating + ActiveCooling + Pool + AntiLegionella + PassiveCooling + Standby + Idle + Off +} diff --git a/src/pump_api/http.gleam b/src/geo_t/pump_api/http.gleam similarity index 94% rename from src/pump_api/http.gleam rename to src/geo_t/pump_api/http.gleam index 6cc2f0f..e1bf0be 100644 --- a/src/pump_api/http.gleam +++ b/src/geo_t/pump_api/http.gleam @@ -1,6 +1,6 @@ import gleam/http/request import gleam/json.{Json} -import pump_api/auth/user.{User} +import geo_t/pump_api/auth/user.{User} pub type Body { Empty diff --git a/src/helpers/application.gleam b/src/helpers/application.gleam deleted file mode 100644 index 5b948b2..0000000 --- a/src/helpers/application.gleam +++ /dev/null @@ -1,8 +0,0 @@ -import gleam/erlang/atom.{Atom} -import gleam/dynamic.{Dynamic} - -pub external fn get_env(Atom, Atom) -> Dynamic = - "Elixir.Application" "get_env" - -pub external fn fetch_env_angry(Atom, Atom) -> Dynamic = - "Elixir.Application" "fetch_env!" diff --git a/src/helpers/binary.gleam b/src/helpers/binary.gleam deleted file mode 100644 index 3d449e6..0000000 --- a/src/helpers/binary.gleam +++ /dev/null @@ -1,2 +0,0 @@ -pub external fn part(data: BitString, pos: Int, len: Int) -> BitString = - "binary" "part" diff --git a/src/helpers/config.gleam b/src/helpers/config.gleam deleted file mode 100644 index c3caae7..0000000 --- a/src/helpers/config.gleam +++ /dev/null @@ -1,37 +0,0 @@ -import gleam/erlang/atom -import gleam/dynamic -import gleam/uri -import helpers/application - -fn app_name() -> atom.Atom { - assert Ok(name) = atom.from_string("geo_therminator") - name -} - -pub fn api_timeout() -> Int { - let timeout = - application.fetch_env_angry( - app_name(), - atom.create_from_string("api_timeout"), - ) - - assert Ok(timeout_int) = dynamic.int(timeout) - timeout_int -} - -pub fn api_auth_url() -> uri.Uri { - config_url("api_auth_url") -} - -pub fn api_installations_url() -> uri.Uri { - config_url("api_installations_url") -} - -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/crypto.gleam b/src/helpers/crypto.gleam deleted file mode 100644 index daf366a..0000000 --- a/src/helpers/crypto.gleam +++ /dev/null @@ -1,7 +0,0 @@ -pub type HashAlgorithm { - // For now, only SHA256 - Sha256 -} - -pub external fn hash(algo: HashAlgorithm, data: String) -> BitString = - "crypto" "hash" diff --git a/src/helpers/date_time.gleam b/src/helpers/date_time.gleam deleted file mode 100644 index 973f041..0000000 --- a/src/helpers/date_time.gleam +++ /dev/null @@ -1,9 +0,0 @@ -import gleam/erlang/atom.{Atom} - -pub external type DateTime - -pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) = - "Elixir.DateTime" "from_iso8601" - -pub external fn from_unix(Int) -> Result(DateTime, #(Atom, Atom)) = - "Elixir.DateTime" "from_unix" diff --git a/src/helpers/io_lib.gleam b/src/helpers/io_lib.gleam deleted file mode 100644 index 5c4487c..0000000 --- a/src/helpers/io_lib.gleam +++ /dev/null @@ -1,4 +0,0 @@ -import gleam/erlang/charlist.{Charlist} - -pub external fn format_int(format: String, data: List(Int)) -> Charlist = - "io_lib" "format" diff --git a/src/pump_api/auth/api.gleam b/src/pump_api/auth/api.gleam deleted file mode 100644 index 310142d..0000000 --- a/src/pump_api/auth/api.gleam +++ /dev/null @@ -1,85 +0,0 @@ -import gleam/http/request -import gleam/json -import gleam/hackney -import gleam/result -import gleam/dynamic -import gleam/list -import gleam/string -import pump_api/auth/user.{User} -import pump_api/auth/tokens.{Tokens} -import pump_api/auth/installation_info.{InstallationInfo} -import pump_api/http -import helpers/config -import helpers/date_time -import helpers/parsing -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) { - try tokens = - b2c.authenticate(username, password) - |> result.map_error(fn(err) { AuthError(inner: err) }) - try access_token_expires_in = - date_time.from_unix(tokens.access_token_expires_in) - |> result.replace_error(InvalidData( - msg: "Access token expiry could not be converted into DateTime: " <> string.inspect( - tokens.access_token_expires_in, - ), - )) - try refresh_token_expires_in = - date_time.from_unix(tokens.refresh_token_expires_in) - |> result.replace_error(InvalidData( - msg: "Refresh token expiry could not be converted into DateTime: " <> string.inspect( - tokens.refresh_token_expires_in, - ), - )) - - Ok(User(tokens: Tokens( - access_token: tokens.access_token, - access_token_expiry: access_token_expires_in, - refresh_token: tokens.refresh_token, - refresh_token_expiry: refresh_token_expires_in, - ))) -} - -pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError) { - let url = config.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 items = - parsing.data_get( - data, - "items", - dynamic.list(of: dynamic.field("id", dynamic.int)), - InvalidData, - ) - - Ok(list.map(items, fn(id) { InstallationInfo(id: id) })) -} - -fn run_req(req: request.Request(String)) { - try resp = - req - |> hackney.send() - |> 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/auth/tokens.gleam b/src/pump_api/auth/tokens.gleam deleted file mode 100644 index 850ac4d..0000000 --- a/src/pump_api/auth/tokens.gleam +++ /dev/null @@ -1,10 +0,0 @@ -import helpers/date_time.{DateTime} - -pub type Tokens { - Tokens( - access_token: String, - access_token_expiry: DateTime, - refresh_token: String, - refresh_token_expiry: DateTime, - ) -} diff --git a/test/geo_therminator_web/views/error_view_test.exs b/test/geo_therminator_web/views/error_view_test.exs deleted file mode 100644 index 18c2daf..0000000 --- a/test/geo_therminator_web/views/error_view_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule GeoTherminatorWeb.ErrorViewTest do - use GeoTherminatorWeb.ConnCase, async: true - - # Bring render/3 and render_to_string/3 for testing custom views - import Phoenix.View - - test "renders 404.html" do - assert render_to_string(GeoTherminatorWeb.ErrorView, "404.html", []) == "Not Found" - end - - test "renders 500.html" do - assert render_to_string(GeoTherminatorWeb.ErrorView, "500.html", []) == "Internal Server Error" - end -end diff --git a/test/geo_therminator_web/views/layout_view_test.exs b/test/geo_therminator_web/views/layout_view_test.exs deleted file mode 100644 index 74d6230..0000000 --- a/test/geo_therminator_web/views/layout_view_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule GeoTherminatorWeb.LayoutViewTest do - use GeoTherminatorWeb.ConnCase, async: true - - # When testing helpers, you may want to import Phoenix.HTML and - # use functions such as safe_to_string() to convert the helper - # result into an HTML string. - # import Phoenix.HTML -end diff --git a/test/geo_therminator_web/views/page_view_test.exs b/test/geo_therminator_web/views/page_view_test.exs deleted file mode 100644 index 86c51d9..0000000 --- a/test/geo_therminator_web/views/page_view_test.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule GeoTherminatorWeb.PageViewTest do - use GeoTherminatorWeb.ConnCase, async: true -end diff --git a/test/test_helper.exs b/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start()