nixos/home-assistant: declarative blueprints

This commit is contained in:
Robert Schütz
2025-01-28 20:00:43 -08:00
parent d616f8f552
commit da7138684b
3 changed files with 88 additions and 2 deletions

View File

@@ -408,6 +408,8 @@
- `services.avahi.ipv6` now defaults to true.
- The Home Assistant module has new options {option}`services.home-assistant.blueprints.automation`, `services.home-assistant.blueprints.script`, and {option}`services.home-assistant.blueprints.template` that allow for the declarative installation of [blueprints](https://www.home-assistant.io/docs/blueprint/) into the appropriate configuration directories.
- For matrix homeserver Synapse we are now following the upstream recommendation to enable jemalloc as the memory allocator by default.
- `services.kmonad` now creates a determinate symlink (in `/dev/input/by-id/`) to each of KMonad virtual devices.

View File

@@ -6,17 +6,22 @@ let
attrByPath
attrValues
concatMap
concatStrings
converge
elem
escapeShellArg
escapeShellArgs
filter
filterAttrsRecursive
flatten
hasAttrByPath
isAttrs
isDerivation
isList
isStorePath
literalExpression
mapAttrsToList
mergeAttrsList
mkEnableOption
mkIf
mkMerge
@@ -28,6 +33,7 @@ let
recursiveUpdate
singleton
splitString
substring
types
unique
;
@@ -502,6 +508,41 @@ in {
type = types.bool;
description = "Whether to open the firewall for the specified port.";
};
blueprints = mergeAttrsList (
map
(domain: {
${domain} = mkOption {
default = [ ];
description = ''
List of ${domain}
[blueprints](https://www.home-assistant.io/docs/blueprint/) to
install into {file}`''${configDir}/blueprints/${domain}`.
'';
example =
if domain == "automation" then
literalExpression ''
[
(pkgs.fetchurl {
url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml";
hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w=";
})
]
''
else if domain == "template" then
literalExpression "[ \"\${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml\" ]"
else
literalExpression "[ ./blueprint.yaml ]";
type = types.listOf (types.coercedTo types.path (x: "${x}") types.pathInStore);
};
})
# https://www.home-assistant.io/docs/blueprint/schema/#domain
[
"automation"
"script"
"template"
]
);
};
config = mkIf cfg.enable {
@@ -576,11 +617,36 @@ in {
ln -fns "''${paths[@]}" "${cfg.configDir}/custom_components/"
done
'';
removeBlueprints = ''
# remove blueprints symlinked in from below the /nix/store
readarray -d "" blueprints < <(find "${cfg.configDir}/blueprints" -maxdepth 2 -type l -print0)
for blueprint in "''${blueprints[@]}"; do
if [[ "$(readlink "$blueprint")" =~ ^${escapeShellArg builtins.storeDir} ]]; then
rm "$blueprint"
fi
done
'';
copyBlueprint =
domain: blueprint:
let
filename =
if isStorePath blueprint then substring 33 (-1) (baseNameOf blueprint) else baseNameOf blueprint;
path = "${cfg.configDir}/blueprints/${domain}";
in
''
mkdir -p ${escapeShellArg path}
ln -s ${escapeShellArg blueprint} ${escapeShellArg "${path}/${filename}"}
'';
copyBlueprints = concatStrings (
flatten (mapAttrsToList (domain: map (copyBlueprint domain)) cfg.blueprints)
);
in
(optionalString (cfg.config != null) copyConfig) +
(optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) +
copyCustomLovelaceModules +
copyCustomComponents
copyCustomComponents +
removeBlueprints +
copyBlueprints
;
environment.PYTHONPATH = package.pythonPath;
serviceConfig = let

View File

@@ -129,6 +129,16 @@ in
];
};
lovelaceConfigWritable = true;
blueprints.automation = [
(pkgs.fetchurl {
url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml";
hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w=";
})
];
blueprints.template = [
"${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml"
];
};
# Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
@@ -148,6 +158,8 @@ in
configuration.services.home-assistant = {
customComponents = lib.mkForce [ ];
customLovelaceModules = lib.mkForce [ ];
blueprints.automation = lib.mkForce [ ];
blueprints.template = lib.mkForce [ ];
};
};
};
@@ -226,6 +238,10 @@ 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 that blueprints are installed"):
hass.succeed("test -L '${configDir}/blueprints/automation/motion_light.yaml'")
hass.succeed("test -L '${configDir}/blueprints/template/inverted_binary_sensor.yaml'")
with subtest("Check service restart from SIGHUP"):
pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
cursor = get_journal_cursor()
@@ -247,12 +263,14 @@ in
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"):
with subtest("Check custom components, custom lovelace modules, and blueprints get removed"):
cursor = get_journal_cursor()
hass.succeed("${system}/specialisation/removeCustomThings/bin/switch-to-configuration test")
hass.fail("grep -q 'mini-graph-card-bundle.js' '${configDir}/ui-lovelace.yaml'")
for integration in ("prometheus_sensor", "spook", "spook_inverse"):
hass.fail(f"test -f ${configDir}/custom_components/{integration}/manifest.json")
hass.fail("test -e '${configDir}/blueprints/automation/motion_light.yaml'")
hass.fail("test -e '${configDir}/blueprints/template/inverted_binary_sensor.yaml'")
wait_for_homeassistant(cursor)
with subtest("Check that no errors were logged"):