diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix index d5af0bcd5e91..599d33724ad3 100644 --- a/nixos/modules/services/home-automation/home-assistant.nix +++ b/nixos/modules/services/home-automation/home-assistant.nix @@ -1,8 +1,41 @@ -{ config, lib, pkgs, ... }: - -with lib; +{ config, lib, pkgs, utils, ... }: let + inherit (lib) + any + attrByPath + attrValues + concatMap + converge + elem + escapeShellArg + escapeShellArgs + filter + filterAttrsRecursive + hasAttrByPath + isAttrs + isDerivation + isList + literalExpression + mkEnableOption + mkIf + mkMerge + mkOption + mkRemovedOptionModule + mkRenamedOptionModule + optionals + optionalString + recursiveUpdate + singleton + splitString + types + unique + ; + + inherit (utils) + escapeSystemdExecArgs + ; + cfg = config.services.home-assistant; format = pkgs.formats.yaml {}; @@ -17,7 +50,7 @@ let # Filter null values from the configuration, so that we can still advertise # optional options in the config attribute. - filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) (lib.recursiveUpdate customLovelaceModulesResources (cfg.config or {})); + filteredConfig = converge (filterAttrsRecursive (_: v: ! elem v [ null ])) (recursiveUpdate customLovelaceModulesResources (cfg.config or {})); configFile = renderYAMLFile "configuration.yaml" filteredConfig; lovelaceConfigFile = renderYAMLFile "ui-lovelace.yaml" cfg.lovelaceConfig; @@ -42,7 +75,7 @@ let if isDerivation config then [ ] else if isAttrs config then - optional (config ? platform) config.platform + optionals (config ? platform) [ config.platform ] ++ concatMap usedPlatforms (attrValues config) else if isList config then concatMap usedPlatforms config @@ -67,7 +100,7 @@ let extraComponents = oldArgs.extraComponents or [] ++ extraComponents; extraPackages = ps: (oldArgs.extraPackages or (_: []) ps) ++ (cfg.extraPackages ps) - ++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents); + ++ (concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents); })); # Create a directory that holds all lovelace modules @@ -93,7 +126,7 @@ in { meta = { buildDocsInSandbox = false; - maintainers = teams.home-assistant.members; + maintainers = lib.teams.home-assistant.members; }; options.services.home-assistant = { @@ -101,6 +134,15 @@ in { # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream"; + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--debug" ]; + description = '' + Extra arguments to pass to the hass executable. + ''; + }; + configDir = mkOption { default = "/var/lib/hass"; type = types.path; @@ -471,12 +513,12 @@ in { networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ]; # symlink the configuration to /etc/home-assistant - environment.etc = lib.mkMerge [ - (lib.mkIf (cfg.config != null && !cfg.configWritable) { + environment.etc = mkMerge [ + (mkIf (cfg.config != null && !cfg.configWritable) { "home-assistant/configuration.yaml".source = configFile; }) - (lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) { + (mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) { "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile; }) ]; @@ -491,8 +533,8 @@ in { "mysql.service" "postgresql.service" ]; - reloadTriggers = lib.optional (cfg.config != null) configFile - ++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile; + reloadTriggers = optionals (cfg.config != null) [ configFile ] + ++ optionals (cfg.lovelaceConfig != null) [ lovelaceConfigFile ]; preStart = let copyConfig = if cfg.configWritable then '' @@ -541,20 +583,20 @@ in { environment.PYTHONPATH = package.pythonPath; serviceConfig = let # List of capabilities to equip home-assistant with, depending on configured components - capabilities = lib.unique ([ + capabilities = unique ([ # Empty string first, so we will never accidentally have an empty capability bounding set # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115 "" - ] ++ lib.optionals (builtins.any useComponent componentsUsingBluetooth) [ + ] ++ optionals (any useComponent componentsUsingBluetooth) [ # Required for interaction with hci devices and bluetooth sockets, identified by bluetooth-adapters dependency # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs "CAP_NET_ADMIN" "CAP_NET_RAW" - ] ++ lib.optionals (useComponent "emulated_hue") [ + ] ++ optionals (useComponent "emulated_hue") [ # Alexa looks for the service on port 80 # https://www.home-assistant.io/integrations/emulated_hue "CAP_NET_BIND_SERVICE" - ] ++ lib.optionals (useComponent "nmap_tracker") [ + ] ++ optionals (useComponent "nmap_tracker") [ # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" @@ -582,12 +624,14 @@ in { "inkbird" "improv_ble" "keymitt_ble" - "leaone-ble" + "ld2410_ble" + "leaone" "led_ble" "medcom_ble" "melnor" "moat" "mopeka" + "motionblinds_ble" "oralb" "private_ble_device" "qingping" @@ -621,25 +665,34 @@ in { # mostly the ones using config flows already. "acer_projector" "alarmdecoder" + "aurora_abb_powerone" "blackbird" + "bryant_evolution" + "crownstone" "deconz" "dsmr" "edl21" "elkm1" "elv" "enocean" + "homeassistant_hardware" + "homeassistant_yellow" "firmata" "flexit" "gpsd" "insteon" "kwb" "lacrosse" + "landisgyr_heat_meter" "modbus" "modem_callerid" "mysensors" "nad" "numato" + "nut" + "opentherm_gw" "otbr" + "rainforst_raven" "rflink" "rfxtrx" "scsgate" @@ -655,12 +708,21 @@ in { "zwave_js" ]; in { - ExecStart = "${package}/bin/hass --config '${cfg.configDir}'"; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = escapeSystemdExecArgs ([ + (lib.getExe package) + "--config" cfg.configDir + ] ++ cfg.extraArgs); + ExecReload = (escapeSystemdExecArgs [ + (lib.getExe' pkgs.coreutils "kill") + "-HUP" + ]) + " $MAINPID"; User = "hass"; Group = "hass"; WorkingDirectory = cfg.configDir; Restart = "on-failure"; + + # Signal handling + # homeassistant/helpers/signal.py RestartForceExitStatus = "100"; SuccessExitStatus = "100"; KillSignal = "SIGINT"; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ab50acd739b9..927382091e72 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -456,7 +456,7 @@ in { hitch = handleTest ./hitch {}; hledger-web = handleTest ./hledger-web.nix {}; hockeypuck = handleTest ./hockeypuck.nix { }; - home-assistant = handleTest ./home-assistant.nix {}; + home-assistant = runTest ./home-assistant.nix; hostname = handleTest ./hostname.nix {}; hound = handleTest ./hound.nix {}; hub = handleTest ./git/hub.nix {}; diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix index 47902fa4e134..7b4e67192f29 100644 --- a/nixos/tests/home-assistant.nix +++ b/nixos/tests/home-assistant.nix @@ -1,4 +1,7 @@ -import ./make-test-python.nix ({ pkgs, lib, ... }: +{ + lib, + ... +}: let configDir = "/var/lib/foobar"; @@ -122,7 +125,7 @@ in { # Cause a configuration change that requires a service restart as we added a new runtime dependency specialisation.newFeature = { inheritParentConfig = true; - configuration.services.home-assistant.config.backup = {}; + configuration.services.home-assistant.config.prometheus = {}; }; specialisation.removeCustomThings = { @@ -206,25 +209,25 @@ in { with subtest("Check extra components are considered in systemd unit hardening"): hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB") - with subtest("Check service reloads when configuration changes"): + with subtest("Check service restart from SIGHUP"): pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") cursor = get_journal_cursor() hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test") - new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") - assert pid == new_pid, "The PID of the process should not change between process reloads" wait_for_homeassistant(cursor) + new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") + assert pid != new_pid, "The PID of the process must change after sending SIGHUP" with subtest("Check service restarts when dependencies change"): pid = new_pid cursor = get_journal_cursor() hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test") - new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") - assert pid != new_pid, "The PID of the process should change when its PYTHONPATH changess" wait_for_homeassistant(cursor) + new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") + assert pid != new_pid, "The PID of the process must change when its PYTHONPATH changess" with subtest("Check that new components get setup after restart"): journal = get_journal_since(cursor) - for domain in ["backup"]: + for domain in ["prometheus"]: assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing" with subtest("Check custom components and custom lovelace modules get removed"): @@ -242,4 +245,4 @@ in { hass.log(hass.succeed("systemctl cat home-assistant.service")) hass.log(hass.succeed("systemd-analyze security home-assistant.service")) ''; -}) +}