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