From d250d1dbef4c341fd0c9a8cc27e6131535b8c7c6 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Mon, 23 Mar 2026 13:05:21 +0100 Subject: [PATCH] write a config module --- .env | 1 + .env.sample | 1 + .gitignore | 2 + README.md | 6 +- config.yml | 5 ++ gleam.toml | 3 + manifest.toml | 8 +++ src/config.gleam | 78 +++++++++++++++++++++++ src/weather_portal.gleam | 14 +++- tasks.txt | 1 + weather_portal/.github/workflows/test.yml | 23 ------- weather_portal/README.md | 24 ------- 12 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 .env create mode 100644 .env.sample create mode 100644 config.yml create mode 100644 src/config.gleam delete mode 100644 weather_portal/.github/workflows/test.yml delete mode 100644 weather_portal/README.md diff --git a/.env b/.env new file mode 100644 index 0000000..365f991 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +export MQTT_PW=Shoa2bu2ungeshimorimooyae6pee8eis5oog5UvahRe4uufeequope0aisahmee diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..23be3f4 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +export MQTT_PW=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/.gitignore b/.gitignore index 599be4e..7902afb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.ez /build erl_crash.dump + +.env diff --git a/README.md b/README.md index ba6b2cb..da7702e 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ This is a real-time, neighborhood-facing weather dashboard that streams sensory data from an MQTT broker (Home Assistant) to a web interface. -Gleam code started created following [this guide](https://gleam.run/writing-gleam/). - ## Tech Stack Overview - **Backend:** Gleam (Target: Erlang/OTP) - **Web Server:** Mist (HTTP & WebSockets) @@ -11,6 +9,10 @@ Gleam code started created following [this guide](https://gleam.run/writing-glea - **Frontend:** SolidJS (Signals-based reactivity) + Tailwind CSS - **Infrastructure:** Nomad (Docker-based deployment) +## Resources + +- https://learnxinyminutes.com/gleam/ +- [Getting started guide](https://gleam.run/writing-gleam/). ## Development diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..d5c7d7c --- /dev/null +++ b/config.yml @@ -0,0 +1,5 @@ +mqtt: + host: "192.168.1.11" + user: "homeassistant" + # password comes from env +jwt_key: apuOQZyML+8PixmVDP4nXU4M # loaded from env on prod diff --git a/gleam.toml b/gleam.toml index f97524d..4e7d401 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,6 +14,9 @@ version = "1.0.0" [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" +simplifile = ">= 2.4.0 and < 3.0.0" +glaml = ">= 3.0.2 and < 4.0.0" +envoy = ">= 1.1.0 and < 2.0.0" [dev_dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index c79796d..891a587 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,18 @@ # You typically do not need to edit this file packages = [ + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "glaml", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "yamerl"], otp_app = "glaml", source = "hex", outer_checksum = "100CA23F526AB159712A3204D200969571FC43B193736B320C1400D410DEE7AD" }, { name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" }, + { name = "yamerl", version = "0.10.0", build_tools = ["rebar3"], requirements = [], otp_app = "yamerl", source = "hex", outer_checksum = "346ADB2963F1051DC837A2364E4ACF6EB7D80097C0F53CBDC3046EC8EC4B4E6E" }, ] [requirements] +envoy = { version = ">= 1.1.0 and < 2.0.0" } +glaml = { version = ">= 3.0.2 and < 4.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +simplifile = { version = ">= 2.4.0 and < 3.0.0" } diff --git a/src/config.gleam b/src/config.gleam new file mode 100644 index 0000000..e60aa98 --- /dev/null +++ b/src/config.gleam @@ -0,0 +1,78 @@ +import envoy +import glaml +import gleam/result +import simplifile + +pub type Config { + Config(mqtt_host: String, mqtt_user: String, mqtt_pw: String, jwt_key: String) +} + +fn load_file() -> Result(String, String) { + simplifile.read("config.yml") + |> result.map_error(fn(_) { "Failed to read config.yml" }) +} + +fn parse_yaml(contents: String) -> Result(glaml.Document, String) { + use docs <- result.try( + glaml.parse_string(contents) + |> result.map_error(fn(_) { "Failed to parse config.yml" }), + ) + + case docs { + [doc] -> Ok(doc) + _ -> Error("Expected exactly one YAML document in config.yml") + } +} + +fn compile_config(doc: glaml.Document) -> Result(Config, String) { + let root = glaml.document_root(doc) + + // Extract values from YAML + use mqtt_host_result <- result.try( + glaml.select_sugar(root, "mqtt.host") + |> result.map_error(fn(_) { "mqtt.host not found in config.yml" }), + ) + use mqtt_user_result <- result.try( + glaml.select_sugar(root, "mqtt.user") + |> result.map_error(fn(_) { "mqtt.user not found in config.yml" }), + ) + use jwt_key_result <- result.try( + glaml.select_sugar(root, "jwt_key") + |> result.map_error(fn(_) { "jwt_key not found in config.yml" }), + ) + + // Extract strings from nodes + case mqtt_host_result, mqtt_user_result, jwt_key_result { + glaml.NodeStr(host), glaml.NodeStr(user), glaml.NodeStr(key) -> { + let yaml_config = + Config( + mqtt_host: host, + mqtt_user: user, + mqtt_pw: "placeholder", + jwt_key: key, + ) + + // Override with env vars + let final_config = + Config( + mqtt_host: envoy.get("MQTT_HOST") + |> result.unwrap(yaml_config.mqtt_host), + mqtt_user: envoy.get("MQTT_USER") + |> result.unwrap(yaml_config.mqtt_user), + mqtt_pw: envoy.get("MQTT_PW") + |> result.unwrap(yaml_config.mqtt_pw), + jwt_key: envoy.get("JWT_KEY") + |> result.unwrap(yaml_config.jwt_key), + ) + + Ok(final_config) + } + _, _, _ -> Error("Config values must be strings in config.yml") + } +} + +pub fn load_config() -> Result(Config, String) { + use file_content <- result.try(load_file()) + use doc <- result.try(parse_yaml(file_content)) + compile_config(doc) +} diff --git a/src/weather_portal.gleam b/src/weather_portal.gleam index 42f6964..623775b 100644 --- a/src/weather_portal.gleam +++ b/src/weather_portal.gleam @@ -1,5 +1,17 @@ +import config import gleam/io pub fn main() -> Nil { - io.println("Hello from weather_portal!") + case config.load_config() { + Ok(cfg) -> { + io.println("Config loaded successfully!") + io.println("MQTT Host: " <> cfg.mqtt_host) + io.println("MQTT User: " <> cfg.mqtt_user) + io.println("MQTT PW: " <> cfg.mqtt_pw) + io.println("JWT Key: " <> cfg.jwt_key) + } + Error(err) -> { + io.println("Failed to load config: " <> err) + } + } } diff --git a/tasks.txt b/tasks.txt index 5195faf..6d4f5c9 100644 --- a/tasks.txt +++ b/tasks.txt @@ -7,5 +7,6 @@ task: hookup a subdomain via reverse proxy ----------------------- +DONE task: write a config module DONE task: install gleam DONE task: get hello world working on local diff --git a/weather_portal/.github/workflows/test.yml b/weather_portal/.github/workflows/test.yml deleted file mode 100644 index 0c036a8..0000000 --- a/weather_portal/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: erlef/setup-beam@v1 - with: - otp-version: "28" - gleam-version: "1.15.2" - rebar3-version: "3" - # elixir-version: "1" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/weather_portal/README.md b/weather_portal/README.md deleted file mode 100644 index 4708d36..0000000 --- a/weather_portal/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# weather_portal - -[![Package Version](https://img.shields.io/hexpm/v/weather_portal)](https://hex.pm/packages/weather_portal) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/weather_portal/) - -```sh -gleam add weather_portal@1 -``` -```gleam -import weather_portal - -pub fn main() -> Nil { - // TODO: An example of the project in use -} -``` - -Further documentation can be found at . - -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -```