Files
homey/modules/services/home-assistant.nix
T
Aner Zakobar c3115026e9 Add storage keepalive (upholds) across services, Nextcloud cron, disable Nextcloud PHP JIT
Requires= doesn't restart a service once its required mount comes back after
a USB disconnect/remount, so a dedicated storage-keepalive unit now Upholds=
every storage-dependent service to bring it back automatically. Also adds the
missing Nextcloud cron.php timer (background jobs were never triggered), and
disables opcache.jit in the Nextcloud container — PHP 8.4's JIT was
miscompiling OC\Config\UserConfig::getValueBool(), crashing every
session-bootstrapping request with a bogus strtolower() TypeError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 09:47:20 +03:00

186 lines
6.6 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{ 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.<domain> → http://localhost:8123.
# LAN devices can also reach HA directly at 192.168.1.100:8123.
#
# Volume layout:
# <dataDir>/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;
}];
};
}