diff --git a/modules/nixos/services/home-assistant/default.nix b/modules/nixos/services/home-assistant/default.nix index 745d2b3..c308c4e 100644 --- a/modules/nixos/services/home-assistant/default.nix +++ b/modules/nixos/services/home-assistant/default.nix @@ -1,5 +1,6 @@ { lib, + pkgs, self, config, namespace, @@ -10,11 +11,38 @@ let inherit (self.lib.options) mkOpt; cfg = config.${namespace}.services.home-assistant; + + yaml = pkgs.formats.yaml { }; + + settings = yaml.generate "configuration.yaml" { + # Includes dependencies for a basic setup + # https://www.home-assistant.io/integrations/default_config/ + default_config = { }; + + http = { + server_host = "::1"; + trusted_proxies = [ "::1" ]; + use_x_forwarded_for = true; + }; + + "automation ui" = "!include automations.yaml"; + "scene ui" = "!include scenes.yaml"; + "script ui" = "!include scripts.yaml"; + }; + + # The YAML generator quotes the custom tags (!include, !secret, ...), which + # makes Home Assistant read them as plain strings. + configuration = pkgs.runCommand "configuration.yaml" { } '' + sed -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;" ${settings} > $out + ''; in { options.${namespace}.services.home-assistant = { enable = mkEnableOption "Home Assistant, the ultimate home automation service."; subdomain = mkOpt types.str "home" "The subdomain the service should be exposed on."; + path = + mkOpt types.str "/var/lib/containers/home-assistant" + "The path this service should use for persistent data."; openthread-device = mkOpt types.str "/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_94A990D18A9C-if00" "The path to your thread border router serial device."; @@ -30,7 +58,8 @@ in nssmdns4 = true; }; - # Additional required service for thread border router support (e.g. ZBT-2) + # Additional required service for thread border router support (e.g. ZBT-2). + # Stays on the host, as it manages the serial radio, host routes and firewall. services.openthread-border-router = { enable = true; @@ -48,55 +77,67 @@ in }; }; - # Additional required service for Matter support - services.matter-server = { - enable = true; + systemd.tmpfiles.rules = [ + "d ${cfg.path} 0755 root root -" + "d ${cfg.path}/config 0755 root root -" + "d ${cfg.path}/matter 0755 root root -" + + # Ensure required files from the above config list are actually available + "f ${cfg.path}/config/automations.yaml 0644 root root -" + "f ${cfg.path}/config/scenes.yaml 0644 root root -" + "f ${cfg.path}/config/scripts.yaml 0644 root root -" + ]; - logLevel = "debug"; - }; + virtualisation.oci-containers.containers = { + home-assistant = { + image = "ghcr.io/home-assistant/home-assistant:2026.8.1"; - services.home-assistant = { - enable = true; + volumes = [ + "${cfg.path}/config:/config" + "${configuration}:/config/configuration.yaml:ro" + "/run/dbus:/run/dbus:ro" + "/etc/localtime:/etc/localtime:ro" + ]; - extraComponents = [ - # Components required to complete the onboarding - "analytics" - "google_translate" - "met" - "radio_browser" - - # For thread/matter with the ZBT-2 - "matter" - "otbr" - "thread" - - # Recommended for fast zlib compression - "isal" - ]; - - config = { - # Includes dependencies for a basic setup - # https://www.home-assistant.io/integrations/default_config/ - default_config = { }; - - http = { - server_host = "::1"; - trusted_proxies = [ "::1" ]; - use_x_forwarded_for = true; + environment = { + "TZ" = "${config.time.timeZone}"; }; - "automation ui" = "!include automations.yaml"; - "scene ui" = "!include scenes.yaml"; - "script ui" = "!include scripts.yaml"; + # Host networking is required for device discovery (mDNS, SSDP) and to + # reach the border router and matter server on ::1 + extraOptions = [ "--network=host" ]; + + dependsOn = [ "matter-server" ]; }; - }; - # Ensure required files from the above config list are actually available - systemd.tmpfiles.rules = [ - "f ${config.services.home-assistant.configDir}/automations.yaml 0644 hass hass" - "f ${config.services.home-assistant.configDir}/scenes.yaml 0644 hass hass" - "f ${config.services.home-assistant.configDir}/scripts.yaml 0644 hass hass" - ]; + # Additional required service for Matter support + matter-server = { + image = "ghcr.io/matter-js/python-matter-server:8.1.2"; + + volumes = [ + "${cfg.path}/matter:/data" + "/run/dbus:/run/dbus:ro" + ]; + + environment = { + "TZ" = "${config.time.timeZone}"; + }; + + cmd = [ + "--storage-path" + "/data" + "--paa-root-cert-dir" + "/data/credentials" + "--log-level" + "debug" + ]; + + extraOptions = [ + "--network=host" + "--security-opt=apparmor=unconfined" + ]; + }; + }; services.traefik.dynamicConfigOptions = { http = {