diff --git a/hosts/server/configuration.nix b/hosts/server/configuration.nix index e061bd81..f987a3b8 100644 --- a/hosts/server/configuration.nix +++ b/hosts/server/configuration.nix @@ -22,7 +22,8 @@ ../../server-modules/fail2ban.nix ../../server-modules/default-creds.nix ../../server-modules/gitea.nix - ../../server-modules/home-assistant.nix + ../../server-modules/signal-cli-rest-api.nix + ../../server-modules/home-assistant # You should let those lines as is ./hardware-configuration.nix diff --git a/hosts/server/secrets/secrets.yaml b/hosts/server/secrets/secrets.yaml index c69ec510..b1948b65 100644 --- a/hosts/server/secrets/secrets.yaml +++ b/hosts/server/secrets/secrets.yaml @@ -9,6 +9,9 @@ signing-pub-key: ENC[AES256_GCM,data:CB7uU2Q4oTEKihpTIXGLaV0fJ1cv/p4oJJ5kjaU6BZi cloudflared-token: ENC[AES256_GCM,data:7DwWFL6JbJXuc6fSyrZkMt/+DAcqDYBQOzJ1QhBL4yfCpQ57mjDV2beRZ8H7SuyoKY3caifHfAVVPDGG1YnffZ9anLRyDW383aHKmgaeztOXj44eVQcHi3evnSiH6h0HasiyO8pNzhnKpYew8CwHSvYZMraX3ITz8UKOTtsanO9/8ZD6Y9gyxfLoySqbeBy2x/0Ql5hrDfrX854C+vYuXM9VJbtQqMBa7+IBydUvddk=,iv:jZTk5ptuwc+1aP2rimqGnyCHR4/J+W4Kyn3LKXXkv/I=,tag:xr/LhoNH5aWdIJr0Py6nAg==,type:str] gitea-mirror-token: ENC[AES256_GCM,data:qIcmwmqIilNtvJPwLMte09p0WAJIfFUl4ZjqoDyx+B9dAf946HTbjQ==,iv:cISz1e6rRlrXp93/GFGhHaeUM4AgdE3iFPxbHVJH950=,tag:Ue1cLdIx78L/G0PzE55hBw==,type:str] github-mirror-token: ENC[AES256_GCM,data:Pty3c2YIZKXuz05j2m2g01cPVdk8nI1ftBzVSt44eXzNJxVEH8rwDw==,iv:hk/M7rp1RlMj0gxXGEESv+VqRZlls/9jpu0jfgq5X4I=,tag:sijScT21VHUpp4Yy6xwy5A==,type:str] +signal_sender_number: ENC[AES256_GCM,data:7eR112vBWgfsY5kX,iv:omEgSZflp/vk28PylbZrsNbdzv2McD3ljB0dr/VcYYk=,tag:x852Hc58eMmLkLLmOSkNdQ==,type:str] +h: ENC[AES256_GCM,data:roBRoH3sAPznAA==,iv:MRZRgKbEEjZosjRH35nYXg04WDXTGUizdVXyhcjQRB8=,tag:ishmf5EXF7UQFE0YoHNPjA==,type:str] +d: ENC[AES256_GCM,data:1XFzLM+rTmJukg==,iv:/tJ+1x7tdmY2+U+l4UPWaj4X49tanLVv/Re3OoEdClc=,tag:99GQ/1PukfQODPJgTlFWdw==,type:str] sops: age: - enc: | @@ -20,7 +23,7 @@ sops: yefODJGVtHrOm3yeXC2xlTi3sFW+c5wUl2yPqddbvcBt5Ud/yd4iXQ== -----END AGE ENCRYPTED FILE----- recipient: age12yvtj49pfh3fqzqflscm0ek4yzrjhr6cqhn7x89gdxnlykq0xudq5c7334 - lastmodified: "2026-09-01T18:31:15Z" - mac: ENC[AES256_GCM,data:ESW+fkZa/yKPIilkeOFBd1mIFl/vgWJpj1mrySjRZ2J9hcVzd02Yu0gG8qeu5b0SnRo4WimvZBfnKnN/E9frgiw+HT9nymVWW0dYOBd3jPRcPE0JkWTsZV0ajvic0o2mCp7/b7bjGoiYxAy/8mbBzKOuG5T3i+ohtumXZ8Y5OIU=,iv:vYjwKVm+UfYF//5VXIdVwRmKkK5AThmPcT7oaM367xs=,tag:UoDIkBxEc4/3PCDB5HKfdQ==,type:str] + lastmodified: "2026-09-04T16:48:30Z" + mac: ENC[AES256_GCM,data:DD4AcqD7rSzo1M/sUcW6SKQgeXFaGZ0z60HDUM1QyTaRDOo2/WCmNG3o3KowRLal/vd8ZxaX6aPmKFYZ4oxYGSvojMXKu2s/EPRtVlx3uv2al0tj8Zg5N1aWvxbrFLjNRb+dQ+8R47Fsmyl54haPz6gRX2qTiJrWL5/YflSDCH0=,iv:5QORWGYcH0NsJ/2+zgrIjHTJBrykpoha5I9thp/0taw=,tag:lKHj77KY9GtFg0VNNGl6mQ==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3 diff --git a/server-modules/home-assistant.nix b/server-modules/home-assistant.nix deleted file mode 100644 index ffd6140b..00000000 --- a/server-modules/home-assistant.nix +++ /dev/null @@ -1,41 +0,0 @@ -{config, ...}: { - services.home-assistant = { - enable = true; - openFirewall = true; - configWritable = true; - extraComponents = [ - "default_config" - "met" - "esphome" - "hue" - "matter" - "thread" - ]; - config = { - default_config = {}; - http = { - trusted_proxies = ["127.0.0.1" "::1"]; - use_x_forwarded_for = true; - }; - scene = "!include scenes.yaml"; - automation = "!include automations.yaml"; - script = "!include scripts.yaml"; - }; - }; - - systemd.services.home-assistant.preStart = '' - for f in scenes.yaml automations.yaml scripts.yaml; do - path="${config.services.home-assistant.configDir}/$f" - [ -f "$path" ] || echo "[]" > "$path" - done - ''; - - services.matter-server = { - enable = true; - extraArgs.primary-interface = config.var.networkInterface; - }; - - networking.firewall.allowedUDPPorts = [5353]; - - services.cloudflared.tunnels."${config.var.tunnelId}".ingress."hass.${config.var.domain}" = "http://localhost:${toString config.services.home-assistant.config.http.server_port}"; -} diff --git a/server-modules/home-assistant/default.nix b/server-modules/home-assistant/default.nix new file mode 100644 index 00000000..bc40aba4 --- /dev/null +++ b/server-modules/home-assistant/default.nix @@ -0,0 +1,94 @@ +{ + config, + lib, + pkgs, + ... +}: let + nixyTheme = import ./theme.nix {inherit config pkgs;}; + plugins = import ./plugins.nix {inherit pkgs;}; + + signalRecipients = [ + "h" + "d" + ]; +in { + sops.secrets = + {signal_sender_number.owner = "hass";} + // lib.genAttrs signalRecipients (_: {owner = "hass";}); + + sops.templates."home-assistant-secrets.yaml" = { + path = "${config.services.home-assistant.configDir}/secrets.yaml"; + owner = "hass"; + restartUnits = ["home-assistant.service"]; + content = + "signal_sender: \"${config.sops.placeholder.signal_sender_number}\"\n" + + lib.concatMapStrings (n: "${n}: \"${config.sops.placeholder.${n}}\"\n") signalRecipients; + }; + + services.home-assistant = { + enable = true; + openFirewall = true; + configWritable = true; + extraComponents = [ + "default_config" + "met" + "esphome" + "hue" + "matter" + "thread" + "sonos" + "apple_tv" + "signal_messenger" + "manual" + "meteo_france" + "remote_calendar" + ]; + customComponents = plugins.customComponents; + customLovelaceModules = plugins.customLovelaceModules; + config = { + default_config = {}; + http = { + trusted_proxies = ["127.0.0.1" "::1"]; + use_x_forwarded_for = true; + }; + scene = "!include scenes.yaml"; + automation = "!include automations.yaml"; + script = "!include scripts.yaml"; + frontend.themes = nixyTheme; + notify = + (map (n: { + name = "signal_${n}"; + platform = "signal_messenger"; + url = config.services.signal-cli-rest-api.url; + number = "!secret signal_sender"; + recipients = ["!secret ${n}"]; + }) + signalRecipients) + ++ [ + { + name = "signal_all"; + platform = "signal_messenger"; + url = config.services.signal-cli-rest-api.url; + number = "!secret signal_sender"; + recipients = map (n: "!secret ${n}") signalRecipients; + } + ]; + }; + }; + + systemd.services.home-assistant.preStart = '' + for f in scenes.yaml automations.yaml scripts.yaml; do + path="${config.services.home-assistant.configDir}/$f" + [ -f "$path" ] || echo "[]" > "$path" + done + ''; + + services.matter-server = { + enable = true; + extraArgs.primary-interface = config.var.networkInterface; + }; + + networking.firewall.allowedUDPPorts = [5353]; + + services.cloudflared.tunnels."${config.var.tunnelId}".ingress."hass.${config.var.domain}" = "http://localhost:${toString config.services.home-assistant.config.http.server_port}"; +} diff --git a/server-modules/home-assistant/plugins.nix b/server-modules/home-assistant/plugins.nix new file mode 100644 index 00000000..0cf8d028 --- /dev/null +++ b/server-modules/home-assistant/plugins.nix @@ -0,0 +1,45 @@ +# Custom (non-core) Home Assistant integrations built from source. +{pkgs, ...}: let + idfm-api = pkgs.home-assistant.python3Packages.buildPythonPackage rec { + pname = "idfm-api"; + version = "1.3.1"; + pyproject = true; + src = pkgs.fetchurl { + url = "https://files.pythonhosted.org/packages/source/i/idfm-api/idfm_api-${version}.tar.gz"; + hash = "sha256-j9whladp+nTUd6dnB0EybOiG2xQ7OyLf6LN3MpzjZv0="; + }; + build-system = [pkgs.home-assistant.python3Packages.setuptools]; + dependencies = with pkgs.home-assistant.python3Packages; [aiohttp async-timeout]; + doCheck = false; + }; + + idfm = pkgs.buildHomeAssistantComponent { + owner = "droso-hass"; + domain = "idfm"; + version = "2.3.1"; + src = pkgs.fetchFromGitHub { + owner = "droso-hass"; + repo = "idfm"; + tag = "v2.3.1"; + hash = "sha256-Zlgd6TXxipcfUcdOyOpLcchM9DO84jCCtREFgbGRcY4="; + }; + dependencies = [idfm-api]; + }; + + atomic-calendar-revive = pkgs.stdenvNoCC.mkDerivation { + pname = "atomic-calendar-revive"; + version = "10.3.1"; + src = pkgs.fetchurl { + url = "https://github.com/totaldebug/atomic-calendar-revive/releases/download/v10.3.1/atomic-calendar-revive.js"; + hash = "sha256-F4vXHQRAsiaK/ylL4X5d70TBxukMAqeQixb6XE68yZo="; + }; + dontUnpack = true; + installPhase = '' + mkdir $out + cp $src $out/atomic-calendar-revive.js + ''; + }; +in { + customComponents = [idfm]; + customLovelaceModules = [atomic-calendar-revive]; +} diff --git a/server-modules/home-assistant/theme.nix b/server-modules/home-assistant/theme.nix new file mode 100644 index 00000000..9b9eeb4e --- /dev/null +++ b/server-modules/home-assistant/theme.nix @@ -0,0 +1,128 @@ +{ + config, + pkgs, + ... +}: let + b = config.stylix.base16Scheme; + hex = shade: "#${shade}"; + rgbTriplet = shade: let + d = c: + if c == "a" + then 10 + else if c == "b" + then 11 + else if c == "c" + then 12 + else if c == "d" + then 13 + else if c == "e" + then 14 + else if c == "f" + then 15 + else pkgs.lib.toInt c; + h = pkgs.lib.toLower shade; + byte = pos: toString (d (builtins.substring pos 1 h) * 16 + d (builtins.substring (pos + 1) 1 h)); + in "${byte 0}, ${byte 2}, ${byte 4}"; +in { + "Nixy" = { + # Main interface colors + "primary-color" = hex b.base0D; + "accent-color" = "var(--primary-color)"; + "primary-background-color" = hex b.base00; + "secondary-background-color" = hex b.base01; + "divider-color" = hex b.base02; + # Text + "primary-text-color" = hex b.base05; + "text-primary-color" = "var(--primary-text-color)"; + "secondary-text-color" = hex b.base04; + "disabled-text-color" = hex b.base03; + # Header / sidebar + "app-header-background-color" = hex b.base01; + "app-header-text-color" = "var(--primary-text-color)"; + "app-header-selection-bar-color" = "var(--primary-color)"; + "app-header-edit-background-color" = hex b.base02; + "sidebar-icon-color" = "var(--secondary-text-color)"; + "sidebar-text-color" = "var(--primary-text-color)"; + "sidebar-background-color" = hex b.base01; + "sidebar-selected-icon-color" = "var(--primary-color)"; + "sidebar-selected-text-color" = "var(--primary-color)"; + # Buttons / icons / state + "paper-item-icon-color" = "var(--secondary-text-color)"; + "paper-item-icon-active-color" = "var(--primary-color)"; + "mdc-button-outline-color" = "var(--primary-color)"; + "state-icon-color" = "var(--paper-item-icon-color)"; + "state-icon-active-color" = "var(--paper-item-icon-active-color)"; + "state-icon-unavailable-color" = "var(--disabled-text-color)"; + # Sliders + "paper-slider-knob-color" = "var(--primary-color)"; + "paper-slider-knob-start-color" = "var(--primary-color)"; + "paper-slider-pin-color" = "var(--primary-color)"; + "paper-slider-active-color" = "var(--primary-color)"; + "paper-slider-secondary-color" = "var(--secondary-background-color)"; + # Labels / badges + "label-badge-background-color" = "var(--secondary-background-color)"; + "label-badge-text-color" = "var(--primary-text-color)"; + "label-badge-red" = hex b.base08; + "label-badge-green" = hex b.base0B; + "label-badge-blue" = hex b.base0D; + "label-badge-yellow" = hex b.base0A; + "label-badge-gray" = "var(--secondary-text-color)"; + # Cards / surfaces + "card-background-color" = hex b.base01; + "ha-card-background" = "var(--card-background-color)"; + "ha-card-border-color" = hex b.base02; + "paper-dialog-background-color" = "var(--card-background-color)"; + "paper-listbox-background-color" = "var(--card-background-color)"; + "paper-card-background-color" = "var(--card-background-color)"; + "table-row-background-color" = "var(--primary-background-color)"; + "table-row-alternative-background-color" = "var(--secondary-background-color)"; + "data-table-background-color" = "var(--primary-background-color)"; + "material-background-color" = "var(--primary-background-color)"; + "material-secondary-background-color" = "var(--secondary-background-color)"; + "markdown-code-background-color" = hex b.base01; + "code-editor-background-color" = hex b.base01; + # Switches / toggles + "switch-checked-button-color" = "var(--primary-color)"; + "switch-checked-track-color" = "var(--primary-color)"; + "switch-unchecked-button-color" = "var(--secondary-text-color)"; + "switch-unchecked-track-color" = "var(--disabled-text-color)"; + "paper-toggle-button-checked-button-color" = "var(--switch-checked-button-color)"; + "paper-toggle-button-checked-bar-color" = "var(--switch-checked-track-color)"; + "paper-toggle-button-unchecked-button-color" = "var(--switch-unchecked-button-color)"; + "paper-toggle-button-unchecked-bar-color" = "var(--switch-unchecked-track-color)"; + "mdc-checkbox-unchecked-color" = "var(--secondary-text-color)"; + "mdc-checkbox-disable-color" = "var(--disabled-text-color)"; + "mdc-theme-primary" = "var(--primary-color)"; + "mdc-theme-secondary" = "var(--primary-color)"; + "mdc-theme-background" = "var(--primary-background-color)"; + "mdc-theme-surface" = "var(--card-background-color)"; + "mdc-theme-on-primary" = hex b.base00; + "mdc-theme-on-secondary" = hex b.base00; + "mdc-theme-on-surface" = "var(--primary-text-color)"; + "mdc-theme-text-primary-on-background" = "var(--primary-text-color)"; + "mdc-theme-text-secondary-on-background" = "var(--secondary-text-color)"; + "mdc-theme-text-hint-on-background" = "var(--secondary-text-color)"; + "mdc-theme-text-disabled-on-background" = "var(--disabled-text-color)"; + "mdc-theme-text-icon-on-background" = "var(--secondary-text-color)"; + "mdc-select-fill-color" = "var(--secondary-background-color)"; + "mdc-select-ink-color" = "var(--primary-text-color)"; + "mdc-select-label-ink-color" = "var(--secondary-text-color)"; + "mdc-select-idle-line-color" = "var(--secondary-text-color)"; + "mdc-select-dropdown-icon-color" = "var(--secondary-text-color)"; + "mdc-select-hover-line-color" = "var(--primary-color)"; + "mdc-list-item-graphic-color" = "var(--secondary-text-color)"; + "mdc-text-field-fill-color" = "var(--mdc-select-fill-color)"; + "mdc-text-field-ink-color" = "var(--primary-text-color)"; + "mdc-text-field-label-ink-color" = "var(--secondary-text-color)"; + "mdc-text-field-idle-line-color" = "var(--secondary-text-color)"; + "input-fill-color" = "var(--secondary-background-color)"; + "input-dropdown-icon-color" = "var(--secondary-text-color)"; + "input-ink-color" = "var(--primary-text-color)"; + "input-label-ink-color" = "var(--secondary-text-color)"; + "input-idle-line-color" = "var(--secondary-text-color)"; + "input-hover-line-color" = "var(--primary-color)"; + "rgb-primary-text-color" = rgbTriplet b.base05; + "rgb-primary-color" = rgbTriplet b.base0D; + "rgb-accent-color" = rgbTriplet b.base0D; + }; +} diff --git a/server-modules/signal-cli-rest-api.nix b/server-modules/signal-cli-rest-api.nix new file mode 100644 index 00000000..7e338229 --- /dev/null +++ b/server-modules/signal-cli-rest-api.nix @@ -0,0 +1,30 @@ +{lib, ...}: let + port = 8080; +in { + options.services.signal-cli-rest-api.url = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = "http://127.0.0.1:${toString port}"; + description = "URL of the local signal-cli-rest-api instance."; + }; + + config = { + virtualisation.docker.enable = true; + + systemd.tmpfiles.rules = ["d /var/lib/signal-cli-rest-api 0750 root root -"]; + + virtualisation.oci-containers = { + backend = "docker"; + containers.signal-cli-rest-api = { + image = "bbernhard/signal-cli-rest-api:0.98"; + # json-rpc keeps one persistent signal-cli daemon instead of spawning + # a fresh process per API call; native mode serializes calls behind a + # file lock and each spawn takes ~40s, causing timeouts under any + # concurrent access (e.g. Home Assistant retries). + environment.MODE = "json-rpc"; + ports = ["127.0.0.1:${toString port}:8080"]; + volumes = ["/var/lib/signal-cli-rest-api:/home/.local/share/signal-cli"]; + }; + }; + }; +}