From 63b4d66ff5d30a0695206d502f65578e47b97cb0 Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Tue, 24 Feb 2026 22:02:50 -0800 Subject: [PATCH] nixos/home-assistant: add themes support --- .../home-automation/home-assistant.nix | 51 ++++++++++++++++++- nixos/tests/home-assistant.nix | 10 ++++ pkgs/servers/home-assistant/themes/README.md | 13 +++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 pkgs/servers/home-assistant/themes/README.md diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix index 34624c3f8b2d..99858a55bb6e 100644 --- a/nixos/modules/services/home-automation/home-assistant.nix +++ b/nixos/modules/services/home-automation/home-assistant.nix @@ -71,7 +71,7 @@ let # 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 { }) + recursiveUpdate (customLovelaceModulesResources // themesConfig) (cfg.config or { }) ); configFile = renderYAMLFile "configuration.yaml" filteredConfig; @@ -143,7 +143,7 @@ let paths = cfg.customLovelaceModules; }; - # Create parts of the lovelace config that reference lovelave modules as resources + # Create parts of the lovelace config that reference lovelace modules as resources customLovelaceModulesResources = { lovelace.resources = map (card: { url = "/local/nixos-lovelace-modules/${card.entrypoint or (card.pname + ".js")}?${card.version}"; @@ -151,6 +151,21 @@ let }) cfg.customLovelaceModules; }; + # Create a directory that holds all lovelace themes + themesDir = pkgs.buildEnv { + name = "home-assistant-themes"; + paths = cfg.themes; + }; + + # Auto-inject frontend.themes include directive when themes are used. + themesConfig = + if cfg.themes != [ ] then + { + frontend.themes = "!include_dir_merge_named ${themesDir}/themes"; + } + else + { }; + componentsUsingBluetooth = [ # Components that require the AF_BLUETOOTH address family "august" @@ -444,6 +459,33 @@ in ''; }; + themes = mkOption { + type = types.listOf ( + types.addCheck types.package (p: p.isHomeAssistantTheme or false) + // { + name = "home-assistant-theme"; + description = "package that is a Home Assistant theme"; + } + ); + default = [ ]; + example = literalExpression '' + with pkgs.home-assistant-themes; [ + material-you-theme + ]; + ''; + description = '' + List of themes to load. + + Available themes can be found below `pkgs.home-assistant-themes`. + + ::: {.note} + When `themes` is set, the module takes authoritative control + over the `frontend.themes` setting in + {option}`services.home-assistant.config`. + ::: + ''; + }; + config = mkOption { type = types.nullOr ( types.submodule { @@ -797,6 +839,10 @@ in assertion = !(cfg.lovelaceConfig != null && cfg.lovelaceConfigFile != null); message = "Only one of `lovelaceConfig` or `lovelaceConfigFile` can be configured at the same time."; } + { + assertion = cfg.themes != [ ] -> !(hasAttrByPath [ "frontend" "themes" ] (cfg.config or { })); + message = "`services.home-assistant.themes` and `services.home-assistant.config.frontend.themes` cannot both be set. When `themes` is non-empty the module sets `frontend.themes` authoritatively."; + } ]; networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ]; @@ -880,6 +926,7 @@ 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) diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix index bc9c173f04d6..fbed5250c7ec 100644 --- a/nixos/tests/home-assistant.nix +++ b/nixos/tests/home-assistant.nix @@ -71,6 +71,11 @@ in mini-graph-card ]; + # test loading themes + themes = with pkgs.home-assistant-themes; [ + material-you-theme + ]; + config = { homeassistant = { name = "Home"; @@ -250,6 +255,11 @@ in hass.succeed("grep -q 'mini-graph-card-bundle.js' '${configDir}/configuration.yaml'") hass.succeed("curl --fail http://localhost:8123/local/nixos-lovelace-modules/mini-graph-card-bundle.js") + with subtest("Check that themes are referenced and installed"): + hass.succeed("grep -q '!include_dir_merge_named.*themes' '${configDir}/configuration.yaml'") + themes_dir = hass.succeed("sed -n 's/.*!include_dir_merge_named \\(.*\\)/\\1/p' '${configDir}/configuration.yaml'").strip() + hass.succeed(f"test -f {themes_dir}/material_you.yaml") + with subtest("Check that optional dependencies are in the PYTHONPATH"): env = get_unit_property("Environment") python_path = env.split("PYTHONPATH=")[1].split()[0] diff --git a/pkgs/servers/home-assistant/themes/README.md b/pkgs/servers/home-assistant/themes/README.md new file mode 100644 index 000000000000..0e43f6e4f4ba --- /dev/null +++ b/pkgs/servers/home-assistant/themes/README.md @@ -0,0 +1,13 @@ +# Packaging guidelines + +## isHomeAssistantTheme + +The nixos module checks for `passthru.isHomeAssistantTheme` on every +package passed to `services.home-assistant.themes`, and rejects packages +that don't have it set. Mark a theme package like this: + +```nix +{ passthru.isHomeAssistantTheme = true; } +``` + +Themes are expected to install their `.yaml` files into `$out/themes/`.