home-assistant: refresh cherry-picks (#363666)

Changed files
+94 -29
nixos
modules
services
home-automation
tests
+81 -19
nixos/modules/services/home-automation/home-assistant.nix
···
-
{ config, lib, pkgs, ... }:
-
-
with lib;
let
cfg = config.services.home-assistant;
format = pkgs.formats.yaml {};
···
# 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 {}));
configFile = renderYAMLFile "configuration.yaml" filteredConfig;
lovelaceConfigFile = renderYAMLFile "ui-lovelace.yaml" cfg.lovelaceConfig;
···
if isDerivation config then
[ ]
else if isAttrs config then
-
optional (config ? platform) config.platform
++ concatMap usedPlatforms (attrValues config)
else if isList config then
concatMap usedPlatforms config
···
extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
++ (cfg.extraPackages ps)
-
++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
}));
# Create a directory that holds all lovelace modules
···
meta = {
buildDocsInSandbox = false;
-
maintainers = teams.home-assistant.members;
};
options.services.home-assistant = {
# Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
# 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";
configDir = mkOption {
default = "/var/lib/hass";
···
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) {
"home-assistant/configuration.yaml".source = configFile;
})
-
(lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
"home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
})
];
···
"mysql.service"
"postgresql.service"
];
-
reloadTriggers = lib.optional (cfg.config != null) configFile
-
++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile;
preStart = let
copyConfig = if cfg.configWritable then ''
···
environment.PYTHONPATH = package.pythonPath;
serviceConfig = let
# List of capabilities to equip home-assistant with, depending on configured components
-
capabilities = lib.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) [
# 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") [
# 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") [
# https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
···
"inkbird"
"improv_ble"
"keymitt_ble"
-
"leaone-ble"
"led_ble"
"medcom_ble"
"melnor"
"moat"
"mopeka"
"oralb"
"private_ble_device"
"qingping"
···
# mostly the ones using config flows already.
"acer_projector"
"alarmdecoder"
"blackbird"
"deconz"
"dsmr"
"edl21"
"elkm1"
"elv"
"enocean"
"firmata"
"flexit"
"gpsd"
"insteon"
"kwb"
"lacrosse"
"modbus"
"modem_callerid"
"mysensors"
"nad"
"numato"
"otbr"
"rflink"
"rfxtrx"
"scsgate"
···
"zwave_js"
];
in {
-
ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
-
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = "hass";
Group = "hass";
WorkingDirectory = cfg.configDir;
Restart = "on-failure";
RestartForceExitStatus = "100";
SuccessExitStatus = "100";
KillSignal = "SIGINT";
···
+
{ 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 {};
···
# Filter null values from the configuration, so that we can still advertise
# optional options in the config attribute.
+
filteredConfig = converge (filterAttrsRecursive (_: v: ! elem v [ null ])) (recursiveUpdate customLovelaceModulesResources (cfg.config or {}));
configFile = renderYAMLFile "configuration.yaml" filteredConfig;
lovelaceConfigFile = renderYAMLFile "ui-lovelace.yaml" cfg.lovelaceConfig;
···
if isDerivation config then
[ ]
else if isAttrs config then
+
optionals (config ? platform) [ config.platform ]
++ concatMap usedPlatforms (attrValues config)
else if isList config then
concatMap usedPlatforms config
···
extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
++ (cfg.extraPackages ps)
+
++ (concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
}));
# Create a directory that holds all lovelace modules
···
meta = {
buildDocsInSandbox = false;
+
maintainers = lib.teams.home-assistant.members;
};
options.services.home-assistant = {
# Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
# 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";
···
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ];
# symlink the configuration to /etc/home-assistant
+
environment.etc = mkMerge [
+
(mkIf (cfg.config != null && !cfg.configWritable) {
"home-assistant/configuration.yaml".source = configFile;
})
+
(mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
"home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
})
];
···
"mysql.service"
"postgresql.service"
];
+
reloadTriggers = optionals (cfg.config != null) [ configFile ]
+
++ optionals (cfg.lovelaceConfig != null) [ lovelaceConfigFile ];
preStart = let
copyConfig = if cfg.configWritable then ''
···
environment.PYTHONPATH = package.pythonPath;
serviceConfig = let
# List of capabilities to equip home-assistant with, depending on configured components
+
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
""
+
] ++ 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"
+
] ++ optionals (useComponent "emulated_hue") [
# Alexa looks for the service on port 80
# https://www.home-assistant.io/integrations/emulated_hue
"CAP_NET_BIND_SERVICE"
+
] ++ optionals (useComponent "nmap_tracker") [
# https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
···
"inkbird"
"improv_ble"
"keymitt_ble"
+
"ld2410_ble"
+
"leaone"
"led_ble"
"medcom_ble"
"melnor"
"moat"
"mopeka"
+
"motionblinds_ble"
"oralb"
"private_ble_device"
"qingping"
···
# 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"
···
"zwave_js"
];
in {
+
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";
+1 -1
nixos/tests/all-tests.nix
···
hitch = handleTest ./hitch {};
hledger-web = handleTest ./hledger-web.nix {};
hockeypuck = handleTest ./hockeypuck.nix { };
-
home-assistant = handleTest ./home-assistant.nix {};
hostname = handleTest ./hostname.nix {};
hound = handleTest ./hound.nix {};
hub = handleTest ./git/hub.nix {};
···
hitch = handleTest ./hitch {};
hledger-web = handleTest ./hledger-web.nix {};
hockeypuck = handleTest ./hockeypuck.nix { };
+
home-assistant = runTest ./home-assistant.nix;
hostname = handleTest ./hostname.nix {};
hound = handleTest ./hound.nix {};
hub = handleTest ./git/hub.nix {};
+12 -9
nixos/tests/home-assistant.nix
···
-
import ./make-test-python.nix ({ pkgs, lib, ... }:
let
configDir = "/var/lib/foobar";
···
# 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 = {};
};
specialisation.removeCustomThings = {
···
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"):
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)
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)
with subtest("Check that new components get setup after restart"):
journal = get_journal_since(cursor)
-
for domain in ["backup"]:
assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
with subtest("Check custom components and custom lovelace modules get removed"):
···
hass.log(hass.succeed("systemctl cat home-assistant.service"))
hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
'';
-
})
···
+
{
+
lib,
+
...
+
}:
let
configDir = "/var/lib/foobar";
···
# 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.prometheus = {};
};
specialisation.removeCustomThings = {
···
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 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")
+
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")
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 ["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"):
···
hass.log(hass.succeed("systemctl cat home-assistant.service"))
hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
'';
+
}