diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix index ce2cf17fab56..55664bfa0d79 100644 --- a/nixos/tests/home-assistant.nix +++ b/nixos/tests/home-assistant.nix @@ -128,16 +128,22 @@ 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" - ]; + # Add blueprints next, because we want to test the installation of the default blueprints first + specialisation.addBlueprints = { + inheritParentConfig = true; + configuration.services.home-assistant = { + 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. @@ -237,7 +243,16 @@ 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"): + with subtest("Check that default blueprints are copied writable"): + hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant | grep 700") + hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant/motion_light.yaml | grep 600") + # Delete blueprints, so we can check the declarative setup next + hass.execute("rm -rf ${configDir}/blueprints") + + with subtest("Check that configured blueprints are installed"): + cursor = get_journal_cursor() + hass.succeed("${system}/specialisation/addBlueprints/bin/switch-to-configuration test") + wait_for_homeassistant(cursor) hass.succeed("test -L '${configDir}/blueprints/automation/motion_light.yaml'") hass.succeed("test -L '${configDir}/blueprints/template/inverted_binary_sensor.yaml'") diff --git a/pkgs/servers/home-assistant/default.nix b/pkgs/servers/home-assistant/default.nix index 8d286ae30426..b6f6b88a5517 100644 --- a/pkgs/servers/home-assistant/default.nix +++ b/pkgs/servers/home-assistant/default.nix @@ -432,6 +432,9 @@ python.pkgs.buildPythonApplication rec { # Follow symlinks in /var/lib/hass/www ./patches/static-follow-symlinks.patch + # Copy default blueprints without preserving permissions + ./patches/default-blueprint-permissions.patch + # Patch path to ffmpeg binary (replaceVars ./patches/ffmpeg-path.patch { ffmpeg = "${lib.getExe ffmpeg-headless}"; diff --git a/pkgs/servers/home-assistant/patches/default-blueprint-permissions.patch b/pkgs/servers/home-assistant/patches/default-blueprint-permissions.patch new file mode 100644 index 000000000000..df802b2828b9 --- /dev/null +++ b/pkgs/servers/home-assistant/patches/default-blueprint-permissions.patch @@ -0,0 +1,35 @@ +commit cca718816f66b0155602c974e6743e8ce49e367b +Author: Martin Weinelt +Date: Thu Jun 12 03:37:27 2025 +0200 + + blueprints: copy default blueprints w/o permissions + + There is no easy way to achieve this with shutil, because copytree() will + always call copystat() an ruin directory permissions. + +diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py +index 88052100259..a0b29ed3d01 100644 +--- a/homeassistant/components/blueprint/models.py ++++ b/homeassistant/components/blueprint/models.py +@@ -378,9 +378,17 @@ class DomainBlueprints: + if self.blueprint_folder.exists(): + return + +- shutil.copytree( +- integration.file_path / BLUEPRINT_FOLDER, +- self.blueprint_folder / HOMEASSISTANT_DOMAIN, +- ) ++ import os ++ os.makedirs(self.blueprint_folder / HOMEASSISTANT_DOMAIN) ++ ++ import subprocess ++ subprocess.check_call([ ++ "cp", ++ "--no-preserve=mode", ++ "--no-target-directory", ++ "--recursive", ++ str(integration.file_path / BLUEPRINT_FOLDER), ++ str(self.blueprint_folder / HOMEASSISTANT_DOMAIN), ++ ]) + + await self.hass.async_add_executor_job(populate)