1 { config, lib, pkgs, ... }:
6 cfg = config.services.home-assistant;
7 format = pkgs.formats.yaml {};
9 # Post-process YAML output to add support for YAML functions, like
10 # secrets or includes, by naively unquoting strings with leading bangs
11 # and at least one space-separated parameter.
12 # https://www.home-assistant.io/docs/configuration/secrets/
13 renderYAMLFile = fn: yaml: pkgs.runCommandLocal fn { } ''
14 cp ${format.generate fn yaml} $out
15 sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
18 # Filter null values from the configuration, so that we can still advertise
19 # optional options in the config attribute.
20 filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) (lib.recursiveUpdate customLovelaceModulesResources (cfg.config or {}));
21 configFile = renderYAMLFile "configuration.yaml" filteredConfig;
23 lovelaceConfigFile = renderYAMLFile "ui-lovelace.yaml" cfg.lovelaceConfig;
25 # Components advertised by the home-assistant package
26 availableComponents = cfg.package.availableComponents;
28 # Components that were added by overriding the package
29 explicitComponents = cfg.package.extraComponents;
30 useExplicitComponent = component: elem component explicitComponents;
32 # Given a component "platform", looks up whether it is used in the config
33 # as `platform = "platform";`.
35 # For example, the component mqtt.sensor is used as follows:
40 usedPlatforms = config:
41 # don't recurse into derivations possibly creating an infinite recursion
42 if isDerivation config then
44 else if isAttrs config then
45 optional (config ? platform) config.platform
46 ++ concatMap usedPlatforms (attrValues config)
47 else if isList config then
48 concatMap usedPlatforms config
51 useComponentPlatform = component: elem component (usedPlatforms cfg.config);
53 # Returns whether component is used in config, explicitly passed into package or
54 # configured in the module.
55 useComponent = component:
56 hasAttrByPath (splitString "." component) cfg.config
57 || useComponentPlatform component
58 || useExplicitComponent component
59 || builtins.elem component (cfg.extraComponents ++ cfg.defaultIntegrations);
61 # Final list of components passed into the package to include required dependencies
62 extraComponents = filter useComponent availableComponents;
64 package = (cfg.package.override (oldArgs: {
65 # Respect overrides that already exist in the passed package and
66 # concat it with values passed via the module.
67 extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
68 extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
69 ++ (cfg.extraPackages ps)
70 ++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
73 # Create a directory that holds all lovelace modules
74 customLovelaceModulesDir = pkgs.buildEnv {
75 name = "home-assistant-custom-lovelace-modules";
76 paths = cfg.customLovelaceModules;
79 # Create parts of the lovelace config that reference lovelave modules as resources
80 customLovelaceModulesResources = {
81 lovelace.resources = map (card: {
82 url = "/local/nixos-lovelace-modules/${card.entrypoint or (card.pname + ".js")}?${card.version}";
84 }) cfg.customLovelaceModules;
88 # Migrations in NixOS 22.05
89 (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
90 (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
91 (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
95 buildDocsInSandbox = false;
96 maintainers = teams.home-assistant.members;
99 options.services.home-assistant = {
100 # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
101 # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
102 enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
104 configDir = mkOption {
105 default = "/var/lib/hass";
107 description = "The config directory, where your {file}`configuration.yaml` is located.";
110 defaultIntegrations = mkOption {
111 type = types.listOf (types.enum availableComponents);
112 # https://github.com/home-assistant/core/blob/dev/homeassistant/bootstrap.py#L109
114 "application_credentials"
145 List of integrations set are always set up, unless in recovery mode.
149 extraComponents = mkOption {
150 type = types.listOf (types.enum availableComponents);
152 # List of components required to complete the onboarding
156 ] ++ optionals pkgs.stdenv.hostPlatform.isAarch [
157 # Use the platform as an indicator that we might be running on a RaspberryPi and include
158 # relevant components
161 example = literalExpression ''
172 List of [components](https://www.home-assistant.io/integrations/) that have their dependencies included in the package.
174 The component name can be found in the URL, for example `https://www.home-assistant.io/integrations/ffmpeg/` would map to `ffmpeg`.
178 extraPackages = mkOption {
179 type = types.functionTo (types.listOf types.package);
181 defaultText = literalExpression ''
182 python3Packages: with python3Packages; [];
184 example = literalExpression ''
185 python3Packages: with python3Packages; [
191 List of packages to add to propagatedBuildInputs.
193 A popular example is `python3Packages.psycopg2`
194 for PostgreSQL support in the recorder component.
198 customComponents = mkOption {
199 type = types.listOf (
200 types.addCheck types.package (p: p.isHomeAssistantComponent or false) // {
201 name = "home-assistant-component";
202 description = "package that is a Home Assistant component";
206 example = literalExpression ''
207 with pkgs.home-assistant-custom-components; [
212 List of custom component packages to install.
214 Available components can be found below `pkgs.home-assistant-custom-components`.
218 customLovelaceModules = mkOption {
219 type = types.listOf types.package;
221 example = literalExpression ''
222 with pkgs.home-assistant-custom-lovelace-modules; [
228 List of custom lovelace card packages to load as lovelace resources.
230 Available cards can be found below `pkgs.home-assistant-custom-lovelace-modules`.
233 Automatic loading only works with lovelace in `yaml` mode.
239 type = types.nullOr (types.submodule {
240 freeformType = format.type;
242 # This is a partial selection of the most common options, so new users can quickly
243 # pick up how to match home-assistants config structure to ours. It also lets us preset
244 # config values intelligently.
247 # https://www.home-assistant.io/docs/configuration/basic/
249 type = types.nullOr types.str;
253 Name of the location where Home Assistant is running.
257 latitude = mkOption {
258 type = types.nullOr (types.either types.float types.str);
262 Latitude of your location required to calculate the time the sun rises and sets.
266 longitude = mkOption {
267 type = types.nullOr (types.either types.float types.str);
271 Longitude of your location required to calculate the time the sun rises and sets.
275 unit_system = mkOption {
276 type = types.nullOr (types.enum [ "metric" "imperial" ]);
280 The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
284 temperature_unit = mkOption {
285 type = types.nullOr (types.enum [ "C" "F" ]);
289 Override temperature unit set by unit_system. `C` for Celsius, `F` for Fahrenheit.
293 time_zone = mkOption {
294 type = types.nullOr types.str;
295 default = config.time.timeZone or null;
296 defaultText = literalExpression ''
297 config.time.timeZone or null
299 example = "Europe/Amsterdam";
301 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).
307 # https://www.home-assistant.io/integrations/http/
308 server_host = mkOption {
309 type = types.either types.str (types.listOf types.str);
316 Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
320 server_port = mkOption {
324 The port on which to listen.
330 # https://www.home-assistant.io/lovelace/dashboards/
332 type = types.enum [ "yaml" "storage" ];
333 default = if cfg.lovelaceConfig != null
336 defaultText = literalExpression ''
337 if cfg.lovelaceConfig != null
343 In what mode should the main Lovelace panel be, `yaml` or `storage` (UI managed).
349 example = literalExpression ''
353 latitude = "!secret latitude";
354 longitude = "!secret longitude";
355 elevation = "!secret elevation";
356 unit_system = "metric";
360 themes = "!include_dir_merge_named themes";
363 feedreader.urls = [ "https://nixos.org/blogs.xml" ];
367 Your {file}`configuration.yaml` as a Nix attribute set.
369 YAML functions like [secrets](https://www.home-assistant.io/docs/configuration/secrets/)
370 can be passed as a string and will be unquoted automatically.
372 Unless this option is explicitly set to `null`
373 we assume your {file}`configuration.yaml` is
374 managed through this module and thereby overwritten on startup.
378 configWritable = mkOption {
382 Whether to make {file}`configuration.yaml` writable.
384 This will allow you to edit it from Home Assistant's web interface.
386 This only has an effect if {option}`config` is set.
387 However, bear in mind that it will be overwritten at every start of the service.
391 lovelaceConfig = mkOption {
393 type = types.nullOr format.type;
394 # from https://www.home-assistant.io/lovelace/dashboards/
395 example = literalExpression ''
397 title = "My Awesome Home";
403 content = "Welcome to your **Lovelace UI**.";
409 Your {file}`ui-lovelace.yaml` as a Nix attribute set.
410 Setting this option will automatically set `lovelace.mode` to `yaml`.
412 Beware that setting this option will delete your previous {file}`ui-lovelace.yaml`
416 lovelaceConfigWritable = mkOption {
420 Whether to make {file}`ui-lovelace.yaml` writable.
422 This will allow you to edit it from Home Assistant's web interface.
424 This only has an effect if {option}`lovelaceConfig` is set.
425 However, bear in mind that it will be overwritten at every start of the service.
430 default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
431 doInstallCheck = false;
433 defaultText = literalExpression ''
434 pkgs.home-assistant.overrideAttrs (oldAttrs: {
435 doInstallCheck = false;
438 type = types.package;
439 example = literalExpression ''
440 pkgs.home-assistant.override {
441 extraPackages = python3Packages: with python3Packages; [
452 The Home Assistant package to use.
456 openFirewall = mkOption {
459 description = "Whether to open the firewall for the specified port.";
463 config = mkIf cfg.enable {
466 assertion = cfg.openFirewall -> cfg.config != null;
467 message = "openFirewall can only be used with a declarative config";
471 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ];
473 # symlink the configuration to /etc/home-assistant
474 environment.etc = lib.mkMerge [
475 (lib.mkIf (cfg.config != null && !cfg.configWritable) {
476 "home-assistant/configuration.yaml".source = configFile;
479 (lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
480 "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
484 systemd.services.home-assistant = {
485 description = "Home Assistant";
486 wants = [ "network-online.target" ];
488 "network-online.target"
490 # prevent races with database creation
494 reloadTriggers = lib.optional (cfg.config != null) configFile
495 ++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile;
498 copyConfig = if cfg.configWritable then ''
499 cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
501 rm -f "${cfg.configDir}/configuration.yaml"
502 ln -s /etc/home-assistant/configuration.yaml "${cfg.configDir}/configuration.yaml"
504 copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
505 rm -f "${cfg.configDir}/ui-lovelace.yaml"
506 cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
508 ln -fs /etc/home-assistant/ui-lovelace.yaml "${cfg.configDir}/ui-lovelace.yaml"
510 copyCustomLovelaceModules = if cfg.customLovelaceModules != [] then ''
511 mkdir -p "${cfg.configDir}/www"
512 ln -fns ${customLovelaceModulesDir} "${cfg.configDir}/www/nixos-lovelace-modules"
514 rm -f "${cfg.configDir}/www/nixos-lovelace-modules"
516 copyCustomComponents = ''
517 mkdir -p "${cfg.configDir}/custom_components"
519 # remove components symlinked in from below the /nix/store
520 readarray -d "" components < <(find "${cfg.configDir}/custom_components" -maxdepth 1 -type l -print0)
521 for component in "''${components[@]}"; do
522 if [[ "$(readlink "$component")" =~ ^${escapeShellArg builtins.storeDir} ]]; then
527 # recreate symlinks for desired components
528 declare -a components=(${escapeShellArgs cfg.customComponents})
529 for component in "''${components[@]}"; do
530 readarray -t manifests < <(find "$component" -name manifest.json)
531 readarray -t paths < <(dirname "''${manifests[@]}")
532 ln -fns "''${paths[@]}" "${cfg.configDir}/custom_components/"
536 (optionalString (cfg.config != null) copyConfig) +
537 (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) +
538 copyCustomLovelaceModules +
541 environment.PYTHONPATH = package.pythonPath;
543 # List of capabilities to equip home-assistant with, depending on configured components
544 capabilities = lib.unique ([
545 # Empty string first, so we will never accidentally have an empty capability bounding set
546 # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
548 ] ++ lib.optionals (builtins.any useComponent componentsUsingBluetooth) [
549 # Required for interaction with hci devices and bluetooth sockets, identified by bluetooth-adapters dependency
550 # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
553 ] ++ lib.optionals (useComponent "emulated_hue") [
554 # Alexa looks for the service on port 80
555 # https://www.home-assistant.io/integrations/emulated_hue
556 "CAP_NET_BIND_SERVICE"
557 ] ++ lib.optionals (useComponent "nmap_tracker") [
558 # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
560 "CAP_NET_BIND_SERVICE"
563 componentsUsingBluetooth = [
564 # Components that require the AF_BLUETOOTH address family
572 "bluetooth_le_tracker"
609 componentsUsingPing = [
610 # Components that require the capset syscall for the ping wrapper
614 componentsUsingSerialDevices = [
615 # Components that require access to serial devices (/dev/tty*)
616 # List generated from home-assistant documentation:
617 # git clone https://github.com/home-assistant/home-assistant.io/
618 # cd source/_integrations
619 # rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
620 # And then extended by references found in the source code, these
621 # mostly the ones using config flows already.
658 ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
659 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
662 WorkingDirectory = cfg.configDir;
663 Restart = "on-failure";
664 RestartForceExitStatus = "100";
665 SuccessExitStatus = "100";
666 KillSignal = "SIGINT";
669 AmbientCapabilities = capabilities;
670 CapabilityBoundingSet = capabilities;
671 DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
676 DevicePolicy = "closed";
677 LockPersonality = true;
678 MemoryDenyWriteExecute = true;
679 NoNewPrivileges = true;
681 PrivateUsers = false; # prevents gaining capabilities in the host namespace
683 ProtectControlGroups = true;
685 ProtectHostname = true;
686 ProtectKernelLogs = true;
687 ProtectKernelModules = true;
688 ProtectKernelTunables = true;
689 ProtectProc = "invisible";
691 ProtectSystem = "strict";
694 # Allow rw access to explicitly configured paths
695 cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
696 value = attrByPath cfgPath [] cfg;
697 allowPaths = if isList value then value else singleton value;
698 in [ "${cfg.configDir}" ] ++ allowPaths;
699 RestrictAddressFamilies = [
704 ] ++ optionals (any useComponent componentsUsingBluetooth) [
707 RestrictNamespaces = true;
708 RestrictRealtime = true;
709 RestrictSUIDSGID = true;
710 SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
713 SystemCallArchitectures = "native";
717 ] ++ optionals (any useComponent componentsUsingPing) [
724 pkgs.unixtools.ping # needed for ping
728 systemd.targets.home-assistant = rec {
729 description = "Home Assistant";
730 wantedBy = [ "multi-user.target" ];
731 wants = [ "home-assistant.service" ];
736 home = cfg.configDir;
739 uid = config.ids.uids.hass;
742 users.groups.hass.gid = config.ids.gids.hass;