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 ])) 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 lovelaceConfig = if (cfg.lovelaceConfig == null) then {}
20 else (lib.recursiveUpdate customLovelaceModulesResources cfg.lovelaceConfig);
21 lovelaceConfigFile = format.generate "ui-lovelace.yaml" lovelaceConfig;
23 # Components advertised by the home-assistant package
24 availableComponents = cfg.package.availableComponents;
26 # Components that were added by overriding the package
27 explicitComponents = cfg.package.extraComponents;
28 useExplicitComponent = component: elem component explicitComponents;
30 # Given a component "platform", looks up whether it is used in the config
31 # as `platform = "platform";`.
33 # For example, the component mqtt.sensor is used as follows:
38 usedPlatforms = config:
39 # don't recurse into derivations possibly creating an infinite recursion
40 if isDerivation config then
42 else if isAttrs config then
43 optional (config ? platform) config.platform
44 ++ concatMap usedPlatforms (attrValues config)
45 else if isList config then
46 concatMap usedPlatforms config
49 useComponentPlatform = component: elem component (usedPlatforms cfg.config);
51 # Returns whether component is used in config, explicitly passed into package or
52 # configured in the module.
53 useComponent = component:
54 hasAttrByPath (splitString "." component) cfg.config
55 || useComponentPlatform component
56 || useExplicitComponent component
57 || builtins.elem component cfg.extraComponents;
59 # Final list of components passed into the package to include required dependencies
60 extraComponents = filter useComponent availableComponents;
62 package = (cfg.package.override (oldArgs: {
63 # Respect overrides that already exist in the passed package and
64 # concat it with values passed via the module.
65 extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
66 extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
67 ++ (cfg.extraPackages ps)
68 ++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
71 # Create a directory that holds all lovelace modules
72 customLovelaceModulesDir = pkgs.buildEnv {
73 name = "home-assistant-custom-lovelace-modules";
74 paths = cfg.customLovelaceModules;
77 # Create parts of the lovelace config that reference lovelave modules as resources
78 customLovelaceModulesResources = {
79 lovelace.resources = map (card: {
80 url = "/local/nixos-lovelace-modules/${card.entrypoint or card.pname}.js?${card.version}";
82 }) cfg.customLovelaceModules;
86 # Migrations in NixOS 22.05
87 (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
88 (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
89 (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
93 buildDocsInSandbox = false;
94 maintainers = teams.home-assistant.members;
97 options.services.home-assistant = {
98 # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
99 # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
100 enable = mkEnableOption (lib.mdDoc "Home Assistant. Please note that this installation method is unsupported upstream");
102 configDir = mkOption {
103 default = "/var/lib/hass";
105 description = lib.mdDoc "The config directory, where your {file}`configuration.yaml` is located.";
108 extraComponents = mkOption {
109 type = types.listOf (types.enum availableComponents);
111 # List of components required to complete the onboarding
115 ] ++ optionals pkgs.stdenv.hostPlatform.isAarch [
116 # Use the platform as an indicator that we might be running on a RaspberryPi and include
117 # relevant components
120 example = literalExpression ''
130 description = lib.mdDoc ''
131 List of [components](https://www.home-assistant.io/integrations/) that have their dependencies included in the package.
133 The component name can be found in the URL, for example `https://www.home-assistant.io/integrations/ffmpeg/` would map to `ffmpeg`.
137 extraPackages = mkOption {
138 type = types.functionTo (types.listOf types.package);
140 defaultText = literalExpression ''
141 python3Packages: with python3Packages; [];
143 example = literalExpression ''
144 python3Packages: with python3Packages; [
149 description = lib.mdDoc ''
150 List of packages to add to propagatedBuildInputs.
152 A popular example is `python3Packages.psycopg2`
153 for PostgreSQL support in the recorder component.
157 customComponents = mkOption {
158 type = types.listOf types.package;
160 example = literalExpression ''
161 with pkgs.home-assistant-custom-components; [
165 description = lib.mdDoc ''
166 List of custom component packages to install.
168 Available components can be found below `pkgs.home-assistant-custom-components`.
172 customLovelaceModules = mkOption {
173 type = types.listOf types.package;
175 example = literalExpression ''
176 with pkgs.home-assistant-custom-lovelace-modules; [
181 description = lib.mdDoc ''
182 List of custom lovelace card packages to load as lovelace resources.
184 Available cards can be found below `pkgs.home-assistant-custom-lovelace-modules`.
187 Automatic loading only works with lovelace in `yaml` mode.
193 type = types.nullOr (types.submodule {
194 freeformType = format.type;
196 # This is a partial selection of the most common options, so new users can quickly
197 # pick up how to match home-assistants config structure to ours. It also lets us preset
198 # config values intelligently.
201 # https://www.home-assistant.io/docs/configuration/basic/
203 type = types.nullOr types.str;
206 description = lib.mdDoc ''
207 Name of the location where Home Assistant is running.
211 latitude = mkOption {
212 type = types.nullOr (types.either types.float types.str);
215 description = lib.mdDoc ''
216 Latitude of your location required to calculate the time the sun rises and sets.
220 longitude = mkOption {
221 type = types.nullOr (types.either types.float types.str);
224 description = lib.mdDoc ''
225 Longitude of your location required to calculate the time the sun rises and sets.
229 unit_system = mkOption {
230 type = types.nullOr (types.enum [ "metric" "imperial" ]);
233 description = lib.mdDoc ''
234 The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
238 temperature_unit = mkOption {
239 type = types.nullOr (types.enum [ "C" "F" ]);
242 description = lib.mdDoc ''
243 Override temperature unit set by unit_system. `C` for Celsius, `F` for Fahrenheit.
247 time_zone = mkOption {
248 type = types.nullOr types.str;
249 default = config.time.timeZone or null;
250 defaultText = literalExpression ''
251 config.time.timeZone or null
253 example = "Europe/Amsterdam";
254 description = lib.mdDoc ''
255 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).
261 # https://www.home-assistant.io/integrations/http/
262 server_host = mkOption {
263 type = types.either types.str (types.listOf types.str);
269 description = lib.mdDoc ''
270 Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
274 server_port = mkOption {
277 description = lib.mdDoc ''
278 The port on which to listen.
284 # https://www.home-assistant.io/lovelace/dashboards/
286 type = types.enum [ "yaml" "storage" ];
287 default = if cfg.lovelaceConfig != null
290 defaultText = literalExpression ''
291 if cfg.lovelaceConfig != null
296 description = lib.mdDoc ''
297 In what mode should the main Lovelace panel be, `yaml` or `storage` (UI managed).
303 example = literalExpression ''
307 latitude = "!secret latitude";
308 longitude = "!secret longitude";
309 elevation = "!secret elevation";
310 unit_system = "metric";
314 themes = "!include_dir_merge_named themes";
317 feedreader.urls = [ "https://nixos.org/blogs.xml" ];
320 description = lib.mdDoc ''
321 Your {file}`configuration.yaml` as a Nix attribute set.
323 YAML functions like [secrets](https://www.home-assistant.io/docs/configuration/secrets/)
324 can be passed as a string and will be unquoted automatically.
326 Unless this option is explicitly set to `null`
327 we assume your {file}`configuration.yaml` is
328 managed through this module and thereby overwritten on startup.
332 configWritable = mkOption {
335 description = lib.mdDoc ''
336 Whether to make {file}`configuration.yaml` writable.
338 This will allow you to edit it from Home Assistant's web interface.
340 This only has an effect if {option}`config` is set.
341 However, bear in mind that it will be overwritten at every start of the service.
345 lovelaceConfig = mkOption {
347 type = types.nullOr format.type;
348 # from https://www.home-assistant.io/lovelace/dashboards/
349 example = literalExpression ''
351 title = "My Awesome Home";
357 content = "Welcome to your **Lovelace UI**.";
362 description = lib.mdDoc ''
363 Your {file}`ui-lovelace.yaml` as a Nix attribute set.
364 Setting this option will automatically set `lovelace.mode` to `yaml`.
366 Beware that setting this option will delete your previous {file}`ui-lovelace.yaml`
370 lovelaceConfigWritable = mkOption {
373 description = lib.mdDoc ''
374 Whether to make {file}`ui-lovelace.yaml` writable.
376 This will allow you to edit it from Home Assistant's web interface.
378 This only has an effect if {option}`lovelaceConfig` is set.
379 However, bear in mind that it will be overwritten at every start of the service.
384 default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
385 doInstallCheck = false;
387 defaultText = literalExpression ''
388 pkgs.home-assistant.overrideAttrs (oldAttrs: {
389 doInstallCheck = false;
392 type = types.package;
393 example = literalExpression ''
394 pkgs.home-assistant.override {
395 extraPackages = python3Packages: with python3Packages; [
405 description = lib.mdDoc ''
406 The Home Assistant package to use.
410 openFirewall = mkOption {
413 description = lib.mdDoc "Whether to open the firewall for the specified port.";
417 config = mkIf cfg.enable {
420 assertion = cfg.openFirewall -> cfg.config != null;
421 message = "openFirewall can only be used with a declarative config";
425 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ];
427 # symlink the configuration to /etc/home-assistant
428 environment.etc = lib.mkMerge [
429 (lib.mkIf (cfg.config != null && !cfg.configWritable) {
430 "home-assistant/configuration.yaml".source = configFile;
433 (lib.mkIf (cfg.lovelaceConfig != null && !cfg.lovelaceConfigWritable) {
434 "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile;
438 systemd.services.home-assistant = {
439 description = "Home Assistant";
441 "network-online.target"
443 # prevent races with database creation
447 reloadTriggers = lib.optional (cfg.config != null) configFile
448 ++ lib.optional (cfg.lovelaceConfig != null) lovelaceConfigFile;
451 copyConfig = if cfg.configWritable then ''
452 cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
454 rm -f "${cfg.configDir}/configuration.yaml"
455 ln -s /etc/home-assistant/configuration.yaml "${cfg.configDir}/configuration.yaml"
457 copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
458 cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
460 rm -f "${cfg.configDir}/ui-lovelace.yaml"
461 ln -s /etc/home-assistant/ui-lovelace.yaml "${cfg.configDir}/ui-lovelace.yaml"
463 copyCustomLovelaceModules = if cfg.customLovelaceModules != [] then ''
464 mkdir -p "${cfg.configDir}/www"
465 ln -fns ${customLovelaceModulesDir} "${cfg.configDir}/www/nixos-lovelace-modules"
467 rm -f "${cfg.configDir}/www/nixos-lovelace-modules"
469 copyCustomComponents = ''
470 mkdir -p "${cfg.configDir}/custom_components"
472 # remove components symlinked in from below the /nix/store
473 components="$(find "${cfg.configDir}/custom_components" -maxdepth 1 -type l)"
474 for component in "$components"; do
475 if [[ "$(readlink "$component")" =~ ^${escapeShellArg builtins.storeDir} ]]; then
480 # recreate symlinks for desired components
481 declare -a components=(${escapeShellArgs cfg.customComponents})
482 for component in "''${components[@]}"; do
483 path="$(dirname $(find "$component" -name "manifest.json"))"
484 ln -fns "$path" "${cfg.configDir}/custom_components/"
488 (optionalString (cfg.config != null) copyConfig) +
489 (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) +
490 copyCustomLovelaceModules +
493 environment.PYTHONPATH = package.pythonPath;
495 # List of capabilities to equip home-assistant with, depending on configured components
496 capabilities = lib.unique ([
497 # Empty string first, so we will never accidentally have an empty capability bounding set
498 # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
500 ] ++ lib.optionals (builtins.any useComponent componentsUsingBluetooth) [
501 # Required for interaction with hci devices and bluetooth sockets, identified by bluetooth-adapters dependency
502 # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
505 ] ++ lib.optionals (useComponent "emulated_hue") [
506 # Alexa looks for the service on port 80
507 # https://www.home-assistant.io/integrations/emulated_hue
508 "CAP_NET_BIND_SERVICE"
509 ] ++ lib.optionals (useComponent "nmap_tracker") [
510 # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
512 "CAP_NET_BIND_SERVICE"
515 componentsUsingBluetooth = [
516 # Components that require the AF_BLUETOOTH address family
524 "bluetooth_le_tracker"
561 componentsUsingPing = [
562 # Components that require the capset syscall for the ping wrapper
566 componentsUsingSerialDevices = [
567 # Components that require access to serial devices (/dev/tty*)
568 # List generated from home-assistant documentation:
569 # git clone https://github.com/home-assistant/home-assistant.io/
570 # cd source/_integrations
571 # rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
572 # And then extended by references found in the source code, these
573 # mostly the ones using config flows already.
610 ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
611 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
614 Restart = "on-failure";
615 RestartForceExitStatus = "100";
616 SuccessExitStatus = "100";
617 KillSignal = "SIGINT";
620 AmbientCapabilities = capabilities;
621 CapabilityBoundingSet = capabilities;
622 DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
627 DevicePolicy = "closed";
628 LockPersonality = true;
629 MemoryDenyWriteExecute = true;
630 NoNewPrivileges = true;
632 PrivateUsers = false; # prevents gaining capabilities in the host namespace
634 ProtectControlGroups = true;
636 ProtectHostname = true;
637 ProtectKernelLogs = true;
638 ProtectKernelModules = true;
639 ProtectKernelTunables = true;
640 ProtectProc = "invisible";
642 ProtectSystem = "strict";
645 # Allow rw access to explicitly configured paths
646 cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
647 value = attrByPath cfgPath [] cfg;
648 allowPaths = if isList value then value else singleton value;
649 in [ "${cfg.configDir}" ] ++ allowPaths;
650 RestrictAddressFamilies = [
655 ] ++ optionals (any useComponent componentsUsingBluetooth) [
658 RestrictNamespaces = true;
659 RestrictRealtime = true;
660 RestrictSUIDSGID = true;
661 SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
664 SystemCallArchitectures = "native";
668 ] ++ optionals (any useComponent componentsUsingPing) [
675 pkgs.unixtools.ping # needed for ping
679 systemd.targets.home-assistant = rec {
680 description = "Home Assistant";
681 wantedBy = [ "multi-user.target" ];
682 wants = [ "home-assistant.service" ];
687 home = cfg.configDir;
690 uid = config.ids.uids.hass;
693 users.groups.hass.gid = config.ids.gids.hass;