{ config, lib, pkgs, homeyConfig, ... }: # Home Assistant — home automation hub. # # Auth model: command_line auth provider → OpenLDAP (primary). # The auth script (ha-ldap-auth.py) does a search-bind as the readonly # service account, then re-binds as the user to verify the password. # A local homeassistant provider is kept as break-glass fallback. # No Authelia forward_auth — HA handles its own sessions and tokens. # # Network: --network=host so mDNS/device-discovery works on the LAN. # Caddy proxies https://home. → http://localhost:8123. # LAN devices can also reach HA directly at 192.168.1.100:8123. # # Volume layout: # /home-assistant/config/ → /config (configuration.yaml, automations…) # # Secrets consumed from sops: # openldap/ro_password (shared with openldap/mealie modules) let cfg = config.homey.homeAssistant; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; # ldap3 and pyasn1 are pure Python. Extract their .py source files from # the PyPI tarballs so the HA container's Python (any version) can import # them. The Nix-compiled site-packages approach fails because everything in # there is a symlink into /nix/store, which doesn't exist inside the container. ldap3Libs = pkgs.runCommand "ldap3-py-libs" {} '' mkdir -p $out cd $TMPDIR mkdir ldap3-extract && cd ldap3-extract tar -xf ${pkgs.python3Packages.ldap3.src} --strip-components=1 cp -r ldap3 $out/ cd $TMPDIR mkdir pyasn1-extract && cd pyasn1-extract tar -xf ${pkgs.python3Packages.pyasn1.src} --strip-components=1 cp -r pyasn1 $out/ ''; # Auth script: shebang uses the container's own Python (/usr/bin/env python3) # and prepends the mounted ldap3 source tree to sys.path at runtime. haLdapAuth = pkgs.writeTextFile { name = "ha-ldap-auth"; executable = true; text = '' #!/usr/bin/env python3 import sys sys.path.insert(0, "/auth/site-packages") '' + builtins.readFile ./ha-ldap-auth.py; }; # Placed in the Nix store; copied to /config only on first boot. initialConfig = pkgs.writeText "ha-initial-configuration.yaml" '' homeassistant: auth_providers: - type: command_line command: /auth/ha-ldap-auth args: [] meta: true - type: homeassistant http: use_x_forwarded_for: true trusted_proxies: - 127.0.0.1 - "::1" mobile_app: ''; in { options.homey.homeAssistant = { enable = lib.mkEnableOption "Home Assistant"; image = lib.mkOption { type = lib.types.str; default = "ghcr.io/home-assistant/home-assistant:stable"; }; port = lib.mkOption { type = lib.types.port; default = 8123; description = "Port HA listens on (host network, so this is a real host port)."; }; }; config = lib.mkIf cfg.enable { # openldap/ro_password is also declared by mealie.nix — NixOS merges them. sops.secrets."openldap/ro_password" = { owner = "root"; }; # ------------------------------------------------------------------------- # Container # ------------------------------------------------------------------------- virtualisation.oci-containers.containers.home-assistant = { image = cfg.image; environment = { TZ = homeyConfig.timezone; }; volumes = [ "${dataDir}/home-assistant/config:/config" "${haLdapAuth}:/auth/ha-ldap-auth:ro" "${ldap3Libs}:/auth/site-packages:ro" "${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap-ro-password:ro" ]; # host network: mDNS / Matter / direct LAN access all require this. # No ports[] mapping needed — HA binds to 0.0.0.0:8123 on the host. extraOptions = [ "--network=host" "--cap-add=NET_ADMIN" "--cap-add=NET_RAW" ]; }; # ------------------------------------------------------------------------- # Systemd service: ordering + first-boot config init # ------------------------------------------------------------------------- systemd.services."podman-home-assistant" = { after = lib.mkAfter [ "mnt-data.mount" ]; requires = lib.mkAfter [ "mnt-data.mount" ]; serviceConfig.ExecStartPre = [ (pkgs.writeShellScript "ha-init-config" '' CFG="${dataDir}/home-assistant/config/configuration.yaml" if [ ! -f "$CFG" ]; then mkdir -p "$(dirname "$CFG")" install -m 644 ${initialConfig} "$CFG" fi '') ]; }; # Open 8123 in the NixOS firewall for direct LAN access. # The HA mobile app connects here directly; Caddy also proxies via this port. networking.firewall.allowedTCPPorts = [ cfg.port ]; # ------------------------------------------------------------------------- # Caddy virtual host — no forward_auth; HA handles its own sessions # ------------------------------------------------------------------------- homey.caddy.virtualHosts = [{ subdomain = "home"; port = cfg.port; auth = false; }]; # ------------------------------------------------------------------------- # Authelia: bypass entirely for home.domain # Priority 18 — blanket bypass range (10–19) # ------------------------------------------------------------------------- homey.authelia.accessControlRules = [{ priority = 18; domain = [ "home.${domain}" ]; policy = "bypass"; }]; # ------------------------------------------------------------------------- # Storage directories # ------------------------------------------------------------------------- homey.storage.upholds = [ "podman-home-assistant.service" ]; homey.storage.extraDirs = [ { path = "home-assistant"; } { path = "home-assistant/config"; mode = "0755"; } ]; # ------------------------------------------------------------------------- # Backup # ------------------------------------------------------------------------- homey.backup.extraPaths = [ "${dataDir}/home-assistant/config" ]; # Uncomment to exclude the recorder SQLite DB if it grows too large: # homey.backup.extraExcludePaths = [ # "${dataDir}/home-assistant/config/home-assistant_v2.db" # ]; # ------------------------------------------------------------------------- # Uptime Kuma monitor # ------------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "Home Assistant"; url = "https://home.${domain}"; interval = 60; }]; }; }