1 { config, lib, pkgs, ... }:
6 cfg = config.services.home-assistant;
7 format = pkgs.formats.yaml {};
9 # Render config attribute sets to YAML
10 # Values that are null will be filtered from the output, so this is one way to have optional
11 # options shown in settings.
12 # We post-process the result to add support for YAML functions, like secrets or includes, see e.g.
13 # https://www.home-assistant.io/docs/configuration/secrets/
14 filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) (lib.recursiveUpdate customLovelaceModulesResources (cfg.config or {}));
15 configFile = pkgs.runCommandLocal "configuration.yaml" { } ''
16 cp ${format.generate "configuration.yaml" filteredConfig} $out
17 sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
19 lovelaceConfigFile = format.generate "ui-lovelace.yaml" cfg.lovelaceConfig;
21 # Components advertised by the home-assistant package
22 availableComponents = cfg.package.availableComponents;
24 # Components that were added by overriding the package
25 explicitComponents = cfg.package.extraComponents;
26 useExplicitComponent = component: elem component explicitComponents;
28 # Given a component "platform", looks up whether it is used in the config
29 # as `platform = "platform";`.
31 # For example, the component mqtt.sensor is used as follows:
36 usedPlatforms = config:
37 # don't recurse into derivations possibly creating an infinite recursion
38 if isDerivation config then
40 else if isAttrs config then
41 optional (config ? platform) config.platform
42 ++ concatMap usedPlatforms (attrValues config)
43 else if isList config then
44 concatMap usedPlatforms config
47 useComponentPlatform = component: elem component (usedPlatforms cfg.config);
49 # Returns whether component is used in config, explicitly passed into package or
50 # configured in the module.
51 useComponent = component:
52 hasAttrByPath (splitString "." component) cfg.config
53 || useComponentPlatform component
54 || useExplicitComponent component
55 || builtins.elem component (cfg.extraComponents ++ cfg.defaultIntegrations);
57 # Final list of components passed into the package to include required dependencies
58 extraComponents = filter useComponent availableComponents;
60 package = (cfg.package.override (oldArgs: {
61 # Respect overrides that already exist in the passed package and
62 # concat it with values passed via the module.
63 extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
64 extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
65 ++ (cfg.extraPackages ps)
66 ++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
69 # Create a directory that holds all lovelace modules
70 customLovelaceModulesDir = pkgs.buildEnv {
71 name = "home-assistant-custom-lovelace-modules";
72 paths = cfg.customLovelaceModules;
75 # Create parts of the lovelace config that reference lovelave modules as resources
76 customLovelaceModulesResources = {
77 lovelace.resources = map (card: {
78 url = "/local/nixos-lovelace-modules/${card.entrypoint or (card.pname + ".js")}?${card.version}";
80 }) cfg.customLovelaceModules;
84 # Migrations in NixOS 22.05
85 (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
86 (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
87 (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
91 buildDocsInSandbox = false;
92 maintainers = teams.home-assistant.members;
95 options.services.home-assistant = {
96 # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
97 # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
98 enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
100 configDir = mkOption {
101 default = "/var/lib/hass";
103 description = "The config directory, where your {file}`configuration.yaml` is located.";
106 defaultIntegrations = mkOption {
107 type = types.listOf (types.enum availableComponents);
108 # https://github.com/home-assistant/core/blob/dev/homeassistant/bootstrap.py#L109
110 "application_credentials"
141 List of integrations set are always set up, unless in recovery mode.
145 extraComponents = mkOption {
146 type = types.listOf (types.enum availableComponents);
148 # List of components required to complete the onboarding
152 ] ++ optionals pkgs.stdenv.hostPlatform.isAarch [
153 # Use the platform as an indicator that we might be running on a RaspberryPi and include
154 # relevant components
157 example = literalExpression ''
168 List of [components](https://www.home-assistant.io/integrations/) that have their dependencies included in the package.
170 The component name can be found in the URL, for example `https://www.home-assistant.io/integrations/ffmpeg/` would map to `ffmpeg`.
174 extraPackages = mkOption {
175 type = types.functionTo (types.listOf types.package);
177 defaultText = literalExpression ''
178 python3Packages: with python3Packages; [];
180 example = literalExpression ''
181 python3Packages: with python3Packages; [
187 List of packages to add to propagatedBuildInputs.
189 A popular example is `python3Packages.psycopg2`
190 for PostgreSQL support in the recorder component.
194 customComponents = mkOption {
195 type = types.listOf (
196 types.addCheck types.package (p: p.isHomeAssistantComponent or false) // {
197 name = "home-assistant-component";
198 description = "package that is a Home Assistant component";
202 example = literalExpression ''
203 with pkgs.home-assistant-custom-components; [
208 List of custom component packages to install.
210 Available components can be found below `pkgs.home-assistant-custom-components`.
214 customLovelaceModules = mkOption {
215 type = types.listOf types.package;
217 example = literalExpression ''
218 with pkgs.home-assistant-custom-lovelace-modules; [
224 List of custom lovelace card packages to load as lovelace resources.
226 Available cards can be found below `pkgs.home-assistant-custom-lovelace-modules`.
229 Automatic loading only works with lovelace in `yaml` mode.
235 type = types.nullOr (types.submodule {
236 freeformType = format.type;
238 # This is a partial selection of the most common options, so new users can quickly
239 # pick up how to match home-assistants config structure to ours. It also lets us preset
240 # config values intelligently.
243 # https://www.home-assistant.io/docs/configuration/basic/
245 type = types.nullOr types.str;
249 Name of the location where Home Assistant is running.
253 latitude = mkOption {
254 type = types.nullOr (types.either types.float types.str);
258 Latitude of your location required to calculate the time the sun rises and sets.
262 longitude = mkOption {
263 type = types.nullOr (types.either types.float types.str);
267 Longitude of your location required to calculate the time the sun rises and sets.
271 unit_system = mkOption {
272 type = types.nullOr (types.enum [ "metric" "imperial" ]);
276 The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
280 temperature_unit = mkOption {
281 type = types.nullOr (types.enum [ "C" "F" ]);
285 Override temperature unit set by unit_system. `C` for Celsius, `F` for Fahrenheit.
289 time_zone = mkOption {
290 type = types.nullOr types.str;
291 default = config.time.timeZone or null;
292 defaultText = literalExpression ''
293 config.time.timeZone or null
295 example = "Europe/Amsterdam";
297 Pick your time zone from the column TZ of Wikipedia’s [list of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
303 # https://www.home-assistant.io/integrations/http/
304 server_host = mkOption {
305 type = types.either types.str (types.listOf types.str);
312 Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
316 server_port = mkOption {
320 The port on which to listen.
326 # https://www.home-assistant.io/lovelace/dashboards/
328 type = types.enum [ "yaml" "storage" ];
329 default = if cfg.lovelaceConfig != null
332 defaultText = literalExpression ''
333 if cfg.lovelaceConfig != null
339 In what mode should the main Lovelace panel be, `yaml` or `storage` (UI managed).
345 example = literalExpression ''
349 latitude = "!secret latitude";
350 longitude = "!secret longitude";
351 elevation = "!secret elevation";
352 unit_system = "metric";
356 themes = "!include_dir_merge_named themes";
359 feedreader.urls = [ "https://nixos.org/blogs.xml" ];
363 Your {file}`configuration.yaml` as a Nix attribute set.
365 YAML functions like [secrets](https://www.home-assistant.io/docs/configuration/secrets/)
366 can be passed as a string and will be unquoted automatically.
368 Unless this option is explicitly set to `null`
369 we assume your {file}`configuration.yaml` is
370 managed through this module and thereby overwritten on startup.
374 configWritable = mkOption {
378 Whether to make {file}`configuration.yaml` writable.
380 This will allow you to edit it from Home Assistant's web interface.
382 This only has an effect if {option}`config` is set.
383 However, bear in mind that it will be overwritten at every start of the service.
387 lovelaceConfig = mkOption {
389 type = types.nullOr format.type;
390 # from https://www.home-assistant.io/lovelace/dashboards/
391 example = literalExpression ''
393 title = "My Awesome Home";
399 content = "Welcome to your **Lovelace UI**.";
405 Your {file}`ui-lovelace.yaml` as a Nix attribute set.
406 Setting this option will automatically set `lovelace.mode` to `yaml`.
408 Beware that setting this option will delete your previous {file}`ui-lovelace.yaml`
412 lovelaceConfigWritable = mkOption {
416 Whether to make {file}`ui-lovelace.yaml` writable.
418 This will allow you to edit it from Home Assistant's web interface.
420 This only has an effect if {option}`lovelaceConfig` is set.
421 However, bear in mind that it will be overwritten at every start of the service.
426 default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
427 doInstallCheck = false;
429 defaultText = literalExpression ''
430 pkgs.home-assistant.overrideAttrs (oldAttrs: {
431 doInstallCheck = false;
434 type = types.package;
435 example = literalExpression ''
436 pkgs.home-assistant.override {
437 extraPackages = python3Packages: with python3Packages; [
448 The Home Assistant package to use.
452 openFirewall = mkOption {
455 description = "Whether to open the firewall for the specified port.";
459 config = mkIf cfg.enable {
462 assertion = cfg.openFirewall -> cfg.config != null;
463 message = "openFirewall can only be used with a declarative config";
467 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ];
469 # symlink the configuration to /etc/home-assistant
470 environment.etc = lib.mkMerge [
471 (lib.mkIf (cfg.config != null && !cfg.configWritable) {
472 "home-assistant/configuration.yaml".source = configFile;
475 (lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
476 "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
480 systemd.services.home-assistant = {
481 description = "Home Assistant";
482 wants = [ "network-online.target" ];
484 "network-online.target"
486 # prevent races with database creation
490 reloadTriggers = lib.optional (cfg.config != null) configFile
491 ++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile;
494 copyConfig = if cfg.configWritable then ''
495 cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
497 rm -f "${cfg.configDir}/configuration.yaml"
498 ln -s /etc/home-assistant/configuration.yaml "${cfg.configDir}/configuration.yaml"
500 copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
501 rm -f "${cfg.configDir}/ui-lovelace.yaml"
502 cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
504 ln -fs /etc/home-assistant/ui-lovelace.yaml "${cfg.configDir}/ui-lovelace.yaml"
506 copyCustomLovelaceModules = if cfg.customLovelaceModules != [] then ''
507 mkdir -p "${cfg.configDir}/www"
508 ln -fns ${customLovelaceModulesDir} "${cfg.configDir}/www/nixos-lovelace-modules"
510 rm -f "${cfg.configDir}/www/nixos-lovelace-modules"
512 copyCustomComponents = ''
513 mkdir -p "${cfg.configDir}/custom_components"
515 # remove components symlinked in from below the /nix/store
516 readarray -d "" components < <(find "${cfg.configDir}/custom_components" -maxdepth 1 -type l -print0)
517 for component in "''${components[@]}"; do
518 if [[ "$(readlink "$component")" =~ ^${escapeShellArg builtins.storeDir} ]]; then
523 # recreate symlinks for desired components
524 declare -a components=(${escapeShellArgs cfg.customComponents})
525 for component in "''${components[@]}"; do
526 readarray -t manifests < <(find "$component" -name manifest.json)
527 readarray -t paths < <(dirname "''${manifests[@]}")
528 ln -fns "''${paths[@]}" "${cfg.configDir}/custom_components/"
532 (optionalString (cfg.config != null) copyConfig) +
533 (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) +
534 copyCustomLovelaceModules +
537 environment.PYTHONPATH = package.pythonPath;
539 # List of capabilities to equip home-assistant with, depending on configured components
540 capabilities = lib.unique ([
541 # Empty string first, so we will never accidentally have an empty capability bounding set
542 # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
544 ] ++ lib.optionals (builtins.any useComponent componentsUsingBluetooth) [
545 # Required for interaction with hci devices and bluetooth sockets, identified by bluetooth-adapters dependency
546 # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
549 ] ++ lib.optionals (useComponent "emulated_hue") [
550 # Alexa looks for the service on port 80
551 # https://www.home-assistant.io/integrations/emulated_hue
552 "CAP_NET_BIND_SERVICE"
553 ] ++ lib.optionals (useComponent "nmap_tracker") [
554 # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
556 "CAP_NET_BIND_SERVICE"
559 componentsUsingBluetooth = [
560 # Components that require the AF_BLUETOOTH address family
568 "bluetooth_le_tracker"
605 componentsUsingPing = [
606 # Components that require the capset syscall for the ping wrapper
610 componentsUsingSerialDevices = [
611 # Components that require access to serial devices (/dev/tty*)
612 # List generated from home-assistant documentation:
613 # git clone https://github.com/home-assistant/home-assistant.io/
614 # cd source/_integrations
615 # rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
616 # And then extended by references found in the source code, these
617 # mostly the ones using config flows already.
654 ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
655 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
658 WorkingDirectory = cfg.configDir;
659 Restart = "on-failure";
660 RestartForceExitStatus = "100";
661 SuccessExitStatus = "100";
662 KillSignal = "SIGINT";
665 AmbientCapabilities = capabilities;
666 CapabilityBoundingSet = capabilities;
667 DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
672 DevicePolicy = "closed";
673 LockPersonality = true;
674 MemoryDenyWriteExecute = true;
675 NoNewPrivileges = true;
677 PrivateUsers = false; # prevents gaining capabilities in the host namespace
679 ProtectControlGroups = true;
681 ProtectHostname = true;
682 ProtectKernelLogs = true;
683 ProtectKernelModules = true;
684 ProtectKernelTunables = true;
685 ProtectProc = "invisible";
687 ProtectSystem = "strict";
690 # Allow rw access to explicitly configured paths
691 cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
692 value = attrByPath cfgPath [] cfg;
693 allowPaths = if isList value then value else singleton value;
694 in [ "${cfg.configDir}" ] ++ allowPaths;
695 RestrictAddressFamilies = [
700 ] ++ optionals (any useComponent componentsUsingBluetooth) [
703 RestrictNamespaces = true;
704 RestrictRealtime = true;
705 RestrictSUIDSGID = true;
706 SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
709 SystemCallArchitectures = "native";
713 ] ++ optionals (any useComponent componentsUsingPing) [
720 pkgs.unixtools.ping # needed for ping
724 systemd.targets.home-assistant = rec {
725 description = "Home Assistant";
726 wantedBy = [ "multi-user.target" ];
727 wants = [ "home-assistant.service" ];
732 home = cfg.configDir;
735 uid = config.ids.uids.hass;
738 users.groups.hass.gid = config.ids.gids.hass;