at master 32 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 10 inherit (lib) 11 any 12 attrByPath 13 attrValues 14 concatMap 15 concatStrings 16 converge 17 elem 18 escapeShellArg 19 escapeShellArgs 20 filter 21 filterAttrsRecursive 22 flatten 23 getAttr 24 hasAttrByPath 25 isAttrs 26 isDerivation 27 isList 28 isStorePath 29 literalExpression 30 mapAttrsToList 31 mergeAttrsList 32 mkEnableOption 33 mkIf 34 mkMerge 35 mkOption 36 mkRemovedOptionModule 37 mkRenamedOptionModule 38 optionals 39 optionalString 40 recursiveUpdate 41 singleton 42 splitString 43 substring 44 types 45 unique 46 ; 47 48 inherit (utils) 49 escapeSystemdExecArgs 50 ; 51 52 cfg = config.services.home-assistant; 53 format = pkgs.formats.yaml { }; 54 55 # Post-process YAML output to add support for YAML functions, like 56 # secrets or includes, by naively unquoting strings with leading bangs 57 # and at least one space-separated parameter. 58 # https://www.home-assistant.io/docs/configuration/secrets/ 59 renderYAMLFile = 60 fn: yaml: 61 pkgs.runCommand fn 62 { 63 preferLocalBuilds = true; 64 } 65 '' 66 cp ${format.generate fn yaml} $out 67 sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out 68 ''; 69 70 # Filter null values from the configuration, so that we can still advertise 71 # optional options in the config attribute. 72 filteredConfig = converge (filterAttrsRecursive (_: v: !elem v [ null ])) ( 73 recursiveUpdate customLovelaceModulesResources (cfg.config or { }) 74 ); 75 configFile = renderYAMLFile "configuration.yaml" filteredConfig; 76 77 lovelaceConfigFile = 78 if cfg.lovelaceConfig != null then 79 renderYAMLFile "ui-lovelace.yaml" cfg.lovelaceConfig 80 else 81 cfg.lovelaceConfigFile; 82 83 # Components advertised by the home-assistant package 84 availableComponents = cfg.package.availableComponents; 85 86 # Components that were added by overriding the package 87 explicitComponents = cfg.package.extraComponents; 88 useExplicitComponent = component: elem component explicitComponents; 89 90 # Given a component "platform", looks up whether it is used in the config 91 # as `platform = "platform";`. 92 # 93 # For example, the component mqtt.sensor is used as follows: 94 # config.sensor = [ { 95 # platform = "mqtt"; 96 # ... 97 # } ]; 98 usedPlatforms = 99 config: 100 # don't recurse into derivations possibly creating an infinite recursion 101 if isDerivation config then 102 [ ] 103 else if isAttrs config then 104 optionals (config ? platform) [ config.platform ] ++ concatMap usedPlatforms (attrValues config) 105 else if isList config then 106 concatMap usedPlatforms config 107 else 108 [ ]; 109 110 useComponentPlatform = component: elem component (usedPlatforms cfg.config); 111 112 # Returns whether component is used in config, explicitly passed into package or 113 # configured in the module. 114 useComponent = 115 component: 116 hasAttrByPath (splitString "." component) cfg.config 117 || useComponentPlatform component 118 || useExplicitComponent component 119 || builtins.elem component ( 120 cfg.extraComponents ++ cfg.defaultIntegrations ++ map (getAttr "domain") cfg.customComponents 121 ); 122 123 # Final list of components passed into the package to include required dependencies 124 extraComponents = filter useComponent availableComponents; 125 126 package = ( 127 cfg.package.override (oldArgs: { 128 # Respect overrides that already exist in the passed package and 129 # concat it with values passed via the module. 130 extraComponents = oldArgs.extraComponents or [ ] ++ extraComponents; 131 extraPackages = 132 ps: 133 (oldArgs.extraPackages or (_: [ ]) ps) 134 ++ (cfg.extraPackages ps) 135 ++ (concatMap (component: component.propagatedBuildInputs or [ ]) cfg.customComponents); 136 }) 137 ); 138 139 # Create a directory that holds all lovelace modules 140 customLovelaceModulesDir = pkgs.buildEnv { 141 name = "home-assistant-custom-lovelace-modules"; 142 paths = cfg.customLovelaceModules; 143 }; 144 145 # Create parts of the lovelace config that reference lovelave modules as resources 146 customLovelaceModulesResources = { 147 lovelace.resources = map (card: { 148 url = "/local/nixos-lovelace-modules/${card.entrypoint or (card.pname + ".js")}?${card.version}"; 149 type = "module"; 150 }) cfg.customLovelaceModules; 151 }; 152in 153{ 154 imports = [ 155 # Migrations in NixOS 22.05 156 (mkRemovedOptionModule [ 157 "services" 158 "home-assistant" 159 "applyDefaultConfig" 160 ] "The default config was migrated into services.home-assistant.config") 161 (mkRemovedOptionModule [ 162 "services" 163 "home-assistant" 164 "autoExtraComponents" 165 ] "Components are now parsed from services.home-assistant.config unconditionally") 166 (mkRenamedOptionModule 167 [ "services" "home-assistant" "port" ] 168 [ "services" "home-assistant" "config" "http" "server_port" ] 169 ) 170 ]; 171 172 meta = { 173 buildDocsInSandbox = false; 174 maintainers = lib.teams.home-assistant.members; 175 }; 176 177 options.services.home-assistant = { 178 # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project. 179 # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision 180 enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream"; 181 182 extraArgs = mkOption { 183 type = types.listOf types.str; 184 default = [ ]; 185 example = [ "--debug" ]; 186 description = '' 187 Extra arguments to pass to the hass executable. 188 ''; 189 }; 190 191 configDir = mkOption { 192 default = "/var/lib/hass"; 193 type = types.path; 194 description = "The config directory, where your {file}`configuration.yaml` is located."; 195 }; 196 197 defaultIntegrations = mkOption { 198 type = types.listOf (types.enum availableComponents); 199 # https://github.com/home-assistant/core/blob/dev/homeassistant/bootstrap.py#L109 200 default = [ 201 "application_credentials" 202 "frontend" 203 "hardware" 204 "logger" 205 "network" 206 "system_health" 207 208 # key features 209 "automation" 210 "person" 211 "scene" 212 "script" 213 "tag" 214 "zone" 215 216 # built-in helpers 217 "counter" 218 "input_boolean" 219 "input_button" 220 "input_datetime" 221 "input_number" 222 "input_select" 223 "input_text" 224 "schedule" 225 "timer" 226 227 # non-supervisor 228 "backup" 229 ]; 230 readOnly = true; 231 description = '' 232 List of integrations set are always set up, unless in recovery mode. 233 ''; 234 }; 235 236 extraComponents = mkOption { 237 type = types.listOf (types.enum availableComponents); 238 default = [ 239 # List of components required to complete the onboarding 240 "default_config" 241 "met" 242 "esphome" 243 ] 244 ++ optionals pkgs.stdenv.hostPlatform.isAarch [ 245 # Use the platform as an indicator that we might be running on a RaspberryPi and include 246 # relevant components 247 "rpi_power" 248 ]; 249 example = literalExpression '' 250 [ 251 "analytics" 252 "default_config" 253 "esphome" 254 "my" 255 "shopping_list" 256 "wled" 257 ] 258 ''; 259 description = '' 260 List of [components](https://www.home-assistant.io/integrations/) that have their dependencies included in the package. 261 262 The component name can be found in the URL, for example `https://www.home-assistant.io/integrations/ffmpeg/` would map to `ffmpeg`. 263 ''; 264 }; 265 266 extraPackages = mkOption { 267 type = types.functionTo (types.listOf types.package); 268 default = _: [ ]; 269 defaultText = literalExpression '' 270 python3Packages: with python3Packages; []; 271 ''; 272 example = literalExpression '' 273 python3Packages: with python3Packages; [ 274 # postgresql support 275 psycopg2 276 ]; 277 ''; 278 description = '' 279 List of packages to add to propagatedBuildInputs. 280 281 A popular example is `python3Packages.psycopg2` 282 for PostgreSQL support in the recorder component. 283 ''; 284 }; 285 286 customComponents = mkOption { 287 type = types.listOf ( 288 types.addCheck types.package (p: p.isHomeAssistantComponent or false) 289 // { 290 name = "home-assistant-component"; 291 description = "package that is a Home Assistant component"; 292 } 293 ); 294 default = [ ]; 295 example = literalExpression '' 296 with pkgs.home-assistant-custom-components; [ 297 prometheus_sensor 298 ]; 299 ''; 300 description = '' 301 List of custom component packages to install. 302 303 Available components can be found below `pkgs.home-assistant-custom-components`. 304 ''; 305 }; 306 307 customLovelaceModules = mkOption { 308 type = types.listOf types.package; 309 default = [ ]; 310 example = literalExpression '' 311 with pkgs.home-assistant-custom-lovelace-modules; [ 312 mini-graph-card 313 mini-media-player 314 ]; 315 ''; 316 description = '' 317 List of custom lovelace card packages to load as lovelace resources. 318 319 Available cards can be found below `pkgs.home-assistant-custom-lovelace-modules`. 320 321 ::: {.note} 322 Automatic loading only works with lovelace in `yaml` mode. 323 ::: 324 ''; 325 }; 326 327 config = mkOption { 328 type = types.nullOr ( 329 types.submodule { 330 freeformType = format.type; 331 options = { 332 # This is a partial selection of the most common options, so new users can quickly 333 # pick up how to match home-assistants config structure to ours. It also lets us preset 334 # config values intelligently. 335 336 homeassistant = { 337 # https://www.home-assistant.io/docs/configuration/basic/ 338 name = mkOption { 339 type = types.nullOr types.str; 340 default = null; 341 example = "Home"; 342 description = '' 343 Name of the location where Home Assistant is running. 344 ''; 345 }; 346 347 latitude = mkOption { 348 type = types.nullOr (types.either types.float types.str); 349 default = null; 350 example = 52.3; 351 description = '' 352 Latitude of your location required to calculate the time the sun rises and sets. 353 ''; 354 }; 355 356 longitude = mkOption { 357 type = types.nullOr (types.either types.float types.str); 358 default = null; 359 example = 4.9; 360 description = '' 361 Longitude of your location required to calculate the time the sun rises and sets. 362 ''; 363 }; 364 365 unit_system = mkOption { 366 type = types.nullOr ( 367 types.enum [ 368 "metric" 369 "us_customary" 370 ] 371 ); 372 default = null; 373 example = "metric"; 374 description = '' 375 The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for US Customary. 376 ''; 377 }; 378 379 temperature_unit = mkOption { 380 type = types.nullOr ( 381 types.enum [ 382 "C" 383 "F" 384 ] 385 ); 386 default = null; 387 example = "C"; 388 description = '' 389 Override temperature unit set by unit_system. `C` for Celsius, `F` for Fahrenheit. 390 ''; 391 }; 392 393 time_zone = mkOption { 394 type = types.nullOr types.str; 395 default = config.time.timeZone or null; 396 defaultText = literalExpression '' 397 config.time.timeZone or null 398 ''; 399 example = "Europe/Amsterdam"; 400 description = '' 401 Pick your time zone from the column TZ of Wikipedias [list of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). 402 ''; 403 }; 404 }; 405 406 http = { 407 # https://www.home-assistant.io/integrations/http/ 408 server_host = mkOption { 409 type = types.either types.str (types.listOf types.str); 410 default = [ 411 "0.0.0.0" 412 "::" 413 ]; 414 example = "::1"; 415 description = '' 416 Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6. 417 ''; 418 }; 419 420 server_port = mkOption { 421 default = 8123; 422 type = types.port; 423 description = '' 424 The port on which to listen. 425 ''; 426 }; 427 }; 428 429 lovelace = { 430 # https://www.home-assistant.io/lovelace/dashboards/ 431 mode = mkOption { 432 type = types.enum [ 433 "yaml" 434 "storage" 435 ]; 436 default = 437 if (cfg.lovelaceConfig != null || cfg.lovelaceConfigFile != null) then "yaml" else "storage"; 438 defaultText = literalExpression '' 439 if (cfg.lovelaceConfig != null || cfg.lovelaceConfigFile != null) 440 then "yaml" 441 else "storage"; 442 ''; 443 example = "yaml"; 444 description = '' 445 In what mode should the main Lovelace panel be, `yaml` or `storage` (UI managed). 446 ''; 447 }; 448 }; 449 }; 450 } 451 ); 452 example = literalExpression '' 453 { 454 homeassistant = { 455 name = "Home"; 456 latitude = "!secret latitude"; 457 longitude = "!secret longitude"; 458 elevation = "!secret elevation"; 459 unit_system = "metric"; 460 time_zone = "UTC"; 461 }; 462 frontend = { 463 themes = "!include_dir_merge_named themes"; 464 }; 465 http = {}; 466 feedreader.urls = [ "https://nixos.org/blogs.xml" ]; 467 } 468 ''; 469 description = '' 470 Your {file}`configuration.yaml` as a Nix attribute set. 471 472 YAML functions like [secrets](https://www.home-assistant.io/docs/configuration/secrets/) 473 can be passed as a string and will be unquoted automatically. 474 475 Unless this option is explicitly set to `null` 476 we assume your {file}`configuration.yaml` is 477 managed through this module and thereby overwritten on startup. 478 ''; 479 }; 480 481 configWritable = mkOption { 482 default = false; 483 type = types.bool; 484 description = '' 485 Whether to make {file}`configuration.yaml` writable. 486 487 This will allow you to edit it from Home Assistant's web interface. 488 489 This only has an effect if {option}`config` is set. 490 However, bear in mind that it will be overwritten at every start of the service. 491 ''; 492 }; 493 494 lovelaceConfig = mkOption { 495 default = null; 496 type = types.nullOr format.type; 497 # from https://www.home-assistant.io/lovelace/dashboards/ 498 example = literalExpression '' 499 { 500 title = "My Awesome Home"; 501 views = [ { 502 title = "Example"; 503 cards = [ { 504 type = "markdown"; 505 title = "Lovelace"; 506 content = "Welcome to your **Lovelace UI**."; 507 } ]; 508 } ]; 509 } 510 ''; 511 description = '' 512 Your {file}`ui-lovelace.yaml` as a Nix attribute set. 513 Setting this option will automatically set `lovelace.mode` to `yaml`. 514 515 Beware that setting this option will delete your previous {file}`ui-lovelace.yaml` 516 ''; 517 }; 518 519 lovelaceConfigFile = mkOption { 520 default = null; 521 type = types.nullOr types.path; 522 example = "/path/to/ui-lovelace.yaml"; 523 description = '' 524 Your {file}`ui-lovelace.yaml` managed as configuraton file. 525 Setting this option will automatically set `lovelace.mode` to `yaml`. 526 ''; 527 }; 528 529 lovelaceConfigWritable = mkOption { 530 default = false; 531 type = types.bool; 532 description = '' 533 Whether to make {file}`ui-lovelace.yaml` writable. 534 535 This will allow you to edit it from Home Assistant's web interface. 536 537 This only has an effect if {option}`lovelaceConfig` is set. 538 However, bear in mind that it will be overwritten at every start of the service. 539 ''; 540 }; 541 542 package = mkOption { 543 default = pkgs.home-assistant.overrideAttrs (oldAttrs: { 544 doInstallCheck = false; 545 }); 546 defaultText = literalExpression '' 547 pkgs.home-assistant.overrideAttrs (oldAttrs: { 548 doInstallCheck = false; 549 }) 550 ''; 551 type = types.package; 552 example = literalExpression '' 553 pkgs.home-assistant.override { 554 extraPackages = python3Packages: with python3Packages; [ 555 psycopg2 556 ]; 557 extraComponents = [ 558 "default_config" 559 "esphome" 560 "met" 561 ]; 562 } 563 ''; 564 description = '' 565 The Home Assistant package to use. 566 ''; 567 }; 568 569 openFirewall = mkOption { 570 default = false; 571 type = types.bool; 572 description = "Whether to open the firewall for the specified port."; 573 }; 574 575 blueprints = mergeAttrsList ( 576 map 577 (domain: { 578 ${domain} = mkOption { 579 default = [ ]; 580 description = '' 581 List of ${domain} 582 [blueprints](https://www.home-assistant.io/docs/blueprint/) to 583 install into {file}`''${config.services.home-assistant.configDir}/blueprints/${domain}`. 584 ''; 585 example = 586 if domain == "automation" then 587 literalExpression '' 588 [ 589 (pkgs.fetchurl { 590 url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml"; 591 hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w="; 592 }) 593 ] 594 '' 595 else if domain == "template" then 596 literalExpression "[ \"\${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml\" ]" 597 else 598 literalExpression "[ ./blueprint.yaml ]"; 599 type = types.listOf (types.coercedTo types.path (x: "${x}") types.pathInStore); 600 }; 601 }) 602 # https://www.home-assistant.io/docs/blueprint/schema/#domain 603 [ 604 "automation" 605 "script" 606 "template" 607 ] 608 ); 609 }; 610 611 config = mkIf cfg.enable { 612 assertions = [ 613 { 614 assertion = cfg.openFirewall -> cfg.config != null; 615 message = "openFirewall can only be used with a declarative config"; 616 } 617 { 618 assertion = !(cfg.lovelaceConfig != null && cfg.lovelaceConfigFile != null); 619 message = "Only one of `lovelaceConfig` or `lovelaceConfigFile` can be configured at the same time."; 620 } 621 ]; 622 623 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.config.http.server_port ]; 624 625 # symlink the configuration to /etc/home-assistant 626 environment.etc = mkMerge [ 627 (mkIf (cfg.config != null && !cfg.configWritable) { 628 "home-assistant/configuration.yaml".source = configFile; 629 }) 630 631 (mkIf 632 ((cfg.lovelaceConfig != null || cfg.lovelaceConfigFile != null) && !cfg.lovelaceConfigWritable) 633 { 634 "home-assistant/ui-lovelace.yaml".source = lovelaceConfigFile; 635 } 636 ) 637 ]; 638 639 systemd.services.home-assistant = { 640 description = "Home Assistant"; 641 wants = [ "network-online.target" ]; 642 after = [ 643 "network-online.target" 644 645 # prevent races with database creation 646 "mysql.service" 647 "postgresql.target" 648 ]; 649 reloadTriggers = 650 optionals (cfg.config != null) [ configFile ] 651 ++ optionals (cfg.lovelaceConfig != null || cfg.lovelaceConfigFile != null) [ lovelaceConfigFile ]; 652 653 preStart = 654 let 655 copyConfig = 656 if cfg.configWritable then 657 '' 658 cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml" 659 '' 660 else 661 '' 662 rm -f "${cfg.configDir}/configuration.yaml" 663 ln -s /etc/home-assistant/configuration.yaml "${cfg.configDir}/configuration.yaml" 664 ''; 665 copyLovelaceConfig = 666 if cfg.lovelaceConfigWritable then 667 '' 668 rm -f "${cfg.configDir}/ui-lovelace.yaml" 669 cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml" 670 '' 671 else 672 '' 673 ln -fs /etc/home-assistant/ui-lovelace.yaml "${cfg.configDir}/ui-lovelace.yaml" 674 ''; 675 copyCustomLovelaceModules = 676 if cfg.customLovelaceModules != [ ] then 677 '' 678 mkdir -p "${cfg.configDir}/www" 679 ln -fns ${customLovelaceModulesDir} "${cfg.configDir}/www/nixos-lovelace-modules" 680 '' 681 else 682 '' 683 rm -f "${cfg.configDir}/www/nixos-lovelace-modules" 684 ''; 685 copyCustomComponents = '' 686 mkdir -p "${cfg.configDir}/custom_components" 687 688 # remove components symlinked in from below the /nix/store 689 readarray -d "" components < <(find "${cfg.configDir}/custom_components" -maxdepth 1 -type l -print0) 690 for component in "''${components[@]}"; do 691 if [[ "$(readlink "$component")" =~ ^${escapeShellArg builtins.storeDir} ]]; then 692 rm "$component" 693 fi 694 done 695 696 # recreate symlinks for desired components 697 declare -a components=(${escapeShellArgs cfg.customComponents}) 698 for component in "''${components[@]}"; do 699 readarray -t manifests < <(find "$component" -name manifest.json) 700 readarray -t paths < <(dirname "''${manifests[@]}") 701 ln -fns "''${paths[@]}" "${cfg.configDir}/custom_components/" 702 done 703 ''; 704 removeBlueprints = '' 705 # remove blueprints symlinked in from below the /nix/store 706 readarray -d "" blueprints < <(find "${cfg.configDir}/blueprints" -maxdepth 2 -type l -print0) 707 for blueprint in "''${blueprints[@]}"; do 708 if [[ "$(readlink "$blueprint")" =~ ^${escapeShellArg builtins.storeDir} ]]; then 709 rm "$blueprint" 710 fi 711 done 712 ''; 713 copyBlueprint = 714 domain: blueprint: 715 let 716 filename = 717 if isStorePath blueprint then substring 33 (-1) (baseNameOf blueprint) else baseNameOf blueprint; 718 path = "${cfg.configDir}/blueprints/${domain}"; 719 in 720 '' 721 mkdir -p ${escapeShellArg path} 722 ln -s ${escapeShellArg blueprint} ${escapeShellArg "${path}/${filename}"} 723 ''; 724 copyBlueprints = concatStrings ( 725 flatten (mapAttrsToList (domain: map (copyBlueprint domain)) cfg.blueprints) 726 ); 727 in 728 (optionalString (cfg.config != null) copyConfig) 729 + (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) 730 + copyCustomLovelaceModules 731 + copyCustomComponents 732 + removeBlueprints 733 + copyBlueprints; 734 environment.PYTHONPATH = package.pythonPath; 735 serviceConfig = 736 let 737 # List of capabilities to equip home-assistant with, depending on configured components 738 capabilities = unique ( 739 [ 740 # Empty string first, so we will never accidentally have an empty capability bounding set 741 # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115 742 "" 743 ] 744 ++ optionals (any useComponent componentsUsingBluetooth) [ 745 # Required for interaction with hci devices and bluetooth sockets, identified by bluetooth-adapters dependency 746 # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs 747 "CAP_NET_ADMIN" 748 "CAP_NET_RAW" 749 ] 750 ++ optionals (useComponent "emulated_hue") [ 751 # Alexa looks for the service on port 80 752 # https://www.home-assistant.io/integrations/emulated_hue 753 "CAP_NET_BIND_SERVICE" 754 ] 755 ++ optionals (useComponent "nmap_tracker") [ 756 # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities 757 "CAP_NET_ADMIN" 758 "CAP_NET_BIND_SERVICE" 759 "CAP_NET_RAW" 760 ] 761 ); 762 componentsUsingBluetooth = [ 763 # Components that require the AF_BLUETOOTH address family 764 "august" 765 "august_ble" 766 "airthings_ble" 767 "aranet" 768 "bluemaestro" 769 "bluetooth" 770 "bluetooth_adapters" 771 "bluetooth_le_tracker" 772 "bluetooth_tracker" 773 "bthome" 774 "default_config" 775 "eufylife_ble" 776 "esphome" 777 "fjaraskupan" 778 "gardena_bluetooth" 779 "govee_ble" 780 "homekit_controller" 781 "inkbird" 782 "improv_ble" 783 "keymitt_ble" 784 "ld2410_ble" 785 "leaone" 786 "led_ble" 787 "medcom_ble" 788 "melnor" 789 "moat" 790 "mopeka" 791 "motionblinds_ble" 792 "oralb" 793 "private_ble_device" 794 "qingping" 795 "rapt_ble" 796 "ruuvi_gateway" 797 "ruuvitag_ble" 798 "sensirion_ble" 799 "sensorpro" 800 "sensorpush" 801 "shelly" 802 "snooz" 803 "switchbot" 804 "thermobeacon" 805 "thermopro" 806 "tilt_ble" 807 "xiaomi_ble" 808 "yalexs_ble" 809 ]; 810 componentsUsingPing = [ 811 # Components that require the capset syscall for the ping wrapper 812 "ping" 813 "wake_on_lan" 814 ]; 815 componentsUsingSerialDevices = [ 816 # Components that require access to serial devices (/dev/tty*) 817 # List generated from home-assistant documentation: 818 # git clone https://github.com/home-assistant/home-assistant.io/ 819 # cd source/_integrations 820 # rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort 821 # And then extended by references found in the source code, these 822 # mostly the ones using config flows already. 823 "acer_projector" 824 "alarmdecoder" 825 "aurora_abb_powerone" 826 "blackbird" 827 "bryant_evolution" 828 "crownstone" 829 "deconz" 830 "dsmr" 831 "edl21" 832 "elkm1" 833 "elv" 834 "enocean" 835 "homeassistant_hardware" 836 "homeassistant_yellow" 837 "firmata" 838 "flexit" 839 "gpsd" 840 "insteon" 841 "kwb" 842 "lacrosse" 843 "landisgyr_heat_meter" 844 "modbus" 845 "modem_callerid" 846 "mysensors" 847 "nad" 848 "numato" 849 "nut" 850 "opentherm_gw" 851 "otbr" 852 "rainforst_raven" 853 "rflink" 854 "rfxtrx" 855 "scsgate" 856 "serial" 857 "serial_pm" 858 "sms" 859 "upb" 860 "usb" 861 "velbus" 862 "w800rf32" 863 "zha" 864 "zwave" 865 "zwave_js" 866 867 # Custom components, maintained manually. 868 "amshan" 869 "benqprojector" 870 ]; 871 componentsUsingInputDevices = [ 872 # Components that require access to input devices (/dev/input/*) 873 "keyboard_remote" 874 ]; 875 in 876 { 877 ExecStart = escapeSystemdExecArgs ( 878 [ 879 (lib.getExe package) 880 "--config" 881 cfg.configDir 882 ] 883 ++ cfg.extraArgs 884 ); 885 ExecReload = 886 (escapeSystemdExecArgs [ 887 (lib.getExe' pkgs.coreutils "kill") 888 "-HUP" 889 ]) 890 + " $MAINPID"; 891 User = "hass"; 892 Group = "hass"; 893 WorkingDirectory = cfg.configDir; 894 Restart = "on-failure"; 895 896 # Signal handling 897 # homeassistant/helpers/signal.py 898 RestartForceExitStatus = "100"; 899 SuccessExitStatus = "100"; 900 KillSignal = "SIGINT"; 901 902 # Hardening 903 AmbientCapabilities = capabilities; 904 CapabilityBoundingSet = capabilities; 905 DeviceAllow = 906 optionals (any useComponent componentsUsingSerialDevices) [ 907 "char-ttyACM rw" 908 "char-ttyAMA rw" 909 "char-ttyUSB rw" 910 ] 911 ++ optionals (any useComponent componentsUsingInputDevices) [ 912 "char-input rw" 913 ]; 914 DevicePolicy = "closed"; 915 LockPersonality = true; 916 MemoryDenyWriteExecute = true; 917 NoNewPrivileges = true; 918 PrivateTmp = true; 919 PrivateUsers = false; # prevents gaining capabilities in the host namespace 920 ProtectClock = true; 921 ProtectControlGroups = true; 922 ProtectHome = true; 923 ProtectHostname = true; 924 ProtectKernelLogs = true; 925 ProtectKernelModules = true; 926 ProtectKernelTunables = true; 927 ProtectProc = "invisible"; 928 ProcSubset = "all"; 929 ProtectSystem = "strict"; 930 RemoveIPC = true; 931 ReadWritePaths = 932 let 933 # Allow rw access to explicitly configured paths 934 cfgPath = [ 935 "config" 936 "homeassistant" 937 "allowlist_external_dirs" 938 ]; 939 value = attrByPath cfgPath [ ] cfg; 940 allowPaths = if isList value then value else singleton value; 941 in 942 [ "${cfg.configDir}" ] ++ allowPaths; 943 RestrictAddressFamilies = [ 944 "AF_INET" 945 "AF_INET6" 946 "AF_NETLINK" 947 "AF_UNIX" 948 ] 949 ++ optionals (any useComponent componentsUsingBluetooth) [ 950 "AF_BLUETOOTH" 951 ]; 952 RestrictNamespaces = true; 953 RestrictRealtime = true; 954 RestrictSUIDSGID = true; 955 SupplementaryGroups = 956 optionals (any useComponent componentsUsingSerialDevices) [ 957 "dialout" 958 ] 959 ++ optionals (any useComponent componentsUsingInputDevices) [ 960 "input" 961 ]; 962 SystemCallArchitectures = "native"; 963 SystemCallFilter = [ 964 "@system-service" 965 "~@privileged" 966 ] 967 ++ optionals (any useComponent componentsUsingPing) [ 968 "capset" 969 "setuid" 970 ]; 971 UMask = "0077"; 972 }; 973 path = [ 974 pkgs.unixtools.ping # needed for ping 975 ]; 976 }; 977 978 systemd.targets.home-assistant = rec { 979 description = "Home Assistant"; 980 wantedBy = [ "multi-user.target" ]; 981 wants = [ "home-assistant.service" ]; 982 after = wants; 983 }; 984 985 users.users.hass = { 986 home = cfg.configDir; 987 createHome = true; 988 group = "hass"; 989 uid = config.ids.uids.hass; 990 }; 991 992 users.groups.hass.gid = config.ids.gids.hass; 993 }; 994}