{ config, pkgs, lib, shb, ... }: let cfg = config.shb.home-assistant; fqdn = "${cfg.subdomain}.${cfg.domain}"; ldap_auth_script_repo = pkgs.fetchFromGitHub { owner = "lldap"; repo = "lldap"; rev = "7d1f5abc137821c500de99c94f7579761fc949d8"; sha256 = "sha256-8D+7ww70Ja6Qwdfa+7MpjAAHewtCWNf/tuTAExoUrg0="; }; ldap_auth_script = pkgs.writeShellScriptBin "ldap_auth.sh" '' export PATH=${pkgs.gnused}/bin:${pkgs.curl}/bin:${pkgs.jq}/bin exec ${pkgs.bash}/bin/bash ${ldap_auth_script_repo}/example_configs/lldap-ha-auth.sh $@ ''; # Filter secrets from config. Secrets are those of the form { source = ; } secrets = lib.attrsets.filterAttrs (k: v: builtins.isAttrs v) cfg.config; nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config); configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets); in { imports = [ ../../lib/module.nix ]; options.shb.home-assistant = { enable = lib.mkEnableOption "selfhostblocks.home-assistant"; subdomain = lib.mkOption { type = lib.types.str; description = "Subdomain under which home-assistant will be served."; example = "ha"; }; domain = lib.mkOption { type = lib.types.str; description = "domain under which home-assistant will be served."; example = "mydomain.com"; }; ssl = lib.mkOption { description = "Path to SSL files"; type = lib.types.nullOr shb.contracts.ssl.certs; default = null; }; config = lib.mkOption { description = "See all available settings at https://www.home-assistant.io/docs/configuration/basic/"; type = lib.types.submodule { freeformType = lib.types.attrsOf lib.types.str; options = { name = lib.mkOption { type = lib.types.oneOf [ lib.types.str shb.secretFileType ]; description = "Name of the Home Assistant instance."; }; country = lib.mkOption { type = lib.types.oneOf [ lib.types.str shb.secretFileType ]; description = "Two letter country code where this instance is located."; }; latitude = lib.mkOption { type = lib.types.oneOf [ lib.types.str shb.secretFileType ]; description = "Latitude where this instance is located."; }; longitude = lib.mkOption { type = lib.types.oneOf [ lib.types.str shb.secretFileType ]; description = "Longitude where this instance is located."; }; time_zone = lib.mkOption { type = lib.types.oneOf [ lib.types.str shb.secretFileType ]; description = "Timezone of this instance."; example = "America/Los_Angeles"; }; unit_system = lib.mkOption { type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ]; description = "Unit system of this instance."; example = "metric"; }; }; }; }; ldap = lib.mkOption { description = '' LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html) Enabling this app will create a new LDAP configuration or update one that exists with the given host. Also, enabling LDAP will skip onboarding otherwise Home Assistant gets into a cyclic lock. ''; default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "LDAP app."; host = lib.mkOption { type = lib.types.str; description = '' Host serving the LDAP server. If set, the Home Assistant auth will be disabled. To keep it, set `keepDefaultAuth` to `true`. ''; default = "127.0.0.1"; }; port = lib.mkOption { type = lib.types.port; description = '' Port of the service serving the LDAP server. ''; default = 389; }; userGroup = lib.mkOption { type = lib.types.str; description = "Group users must belong to to be able to login to Nextcloud."; default = "homeassistant_user"; }; keepDefaultAuth = lib.mkOption { type = lib.types.bool; description = '' Keep Home Assistant auth active, even if LDAP is configured. Usually, you want to enable this to transfer existing users to LDAP and then you can disabled it. ''; default = false; }; }; }; }; voice = lib.mkOption { description = "Options related to voice service."; default = { }; type = lib.types.submodule { options = { speech-to-text = lib.mkOption { description = '' Wyoming piper servers. https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.piper.servers ''; type = lib.types.attrsOf lib.types.anything; default = { }; }; text-to-speech = lib.mkOption { description = '' Wyoming faster-whisper servers. https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.faster-whisper.servers ''; type = lib.types.attrsOf lib.types.anything; default = { }; }; wakeword = lib.mkOption { description = '' Wyoming open wakework servers. https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.openwakeword ''; type = lib.types.anything; default = { enable = false; }; }; }; }; }; backup = lib.mkOption { description = '' Backup configuration. ''; default = { }; type = lib.types.submodule { options = shb.contracts.backup.mkRequester { user = "hass"; # No need for backup hooks as we use an hourly automation job in home assistant directly with a cron job. sourceDirectories = [ "/var/lib/hass/backups" ]; }; }; }; dashboard = lib.mkOption { description = '' Dashboard contract consumer ''; default = { }; type = lib.types.submodule { options = shb.contracts.dashboard.mkRequester { externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; externalUrlText = "https://\${config.shb.home-assistant.subdomain}.\${config.shb.home-assistant.domain}"; internalUrl = "http://127.0.0.1:${toString config.services.home-assistant.config.http.server_port}"; internalUrlText = "http://127.0.0.1:\${config.services.home-assistant.config.http.server_port}"; }; }; }; }; config = lib.mkIf cfg.enable { services.home-assistant = { enable = true; # Find them at https://github.com/NixOS/nixpkgs/blob/master/pkgs/servers/home-assistant/component-packages.nix extraComponents = [ # Components required to complete the onboarding "met" "radio_browser" ]; configDir = "/var/lib/hass"; # If you can't find a component in component-packages.nix, you can add them manually with something similar to: # extraPackages = python3Packages: [ # (python3Packages.simplisafe-python.overrideAttrs (old: rec { # pname = "simplisafe-python"; # version = "5b003a9fa1abd00f0e9a0b99d3ee57c4c7c16bda"; # format = "pyproject"; # src = pkgs.fetchFromGitHub { # owner = "bachya"; # repo = pname; # rev = "${version}"; # hash = "sha256-Ij2e0QGYLjENi/yhFBQ+8qWEJp86cgwC9E27PQ5xNno="; # }; # })) # ]; config = { # Includes dependencies for a basic setup # https://www.home-assistant.io/integrations/default_config/ http = { use_x_forwarded_for = true; server_host = "127.0.0.1"; server_port = 8123; trusted_proxies = "127.0.0.1"; }; logger.default = "info"; homeassistant = configWithSecretsIncludes // { external_url = "https://${cfg.subdomain}.${cfg.domain}"; internal_url = "https://${cfg.subdomain}.${cfg.domain}"; auth_providers = (lib.optionals (!cfg.ldap.enable || cfg.ldap.keepDefaultAuth) [ { type = "homeassistant"; } ]) ++ (lib.optionals cfg.ldap.enable [ { type = "command_line"; command = ldap_auth_script + "/bin/ldap_auth.sh"; args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup ]; meta = true; } ]); }; "automation ui" = "!include automations.yaml"; "scene ui" = "!include scenes.yaml"; "script ui" = "!include scripts.yaml"; "automation manual" = [ { alias = "Create Backup on Schedule"; trigger = [ { platform = "time_pattern"; minutes = "5"; } ]; action = [ { service = "shell_command.delete_backups"; data = { }; } { service = "backup.create"; data = { }; } ]; mode = "single"; } ]; shell_command = { delete_backups = "find ${config.services.home-assistant.configDir}/backups -type f -delete"; }; conversation.intents = { TellJoke = [ "Tell [me] (a joke|something funny|a dad joke)" "Raconte [moi] (une blague)" ]; }; sensor = [ { name = "random_joke"; platform = "rest"; json_attributes = [ "joke" "id" "status" ]; value_template = "{{ value_json.joke }}"; resource = "https://icanhazdadjoke.com/"; scan_interval = "3600"; headers.Accept = "application/json"; } ]; intent_script.TellJoke = { speech.text = ''{{ state_attr("sensor.random_joke", "joke") }}''; action = { service = "homeassistant.update_entity"; entity_id = "sensor.random_joke"; }; }; }; }; services.wyoming.piper.servers = cfg.voice.text-to-speech; services.wyoming.faster-whisper.servers = cfg.voice.speech-to-text; services.wyoming.openwakeword = cfg.voice.wakeword; services.nginx.virtualHosts."${fqdn}" = { http2 = true; forceSSL = !(isNull cfg.ssl); sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; extraConfig = '' proxy_buffering off; ''; locations."/" = { proxyPass = "http://${toString config.services.home-assistant.config.http.server_host}:${toString config.services.home-assistant.config.http.server_port}/"; proxyWebsockets = true; }; }; systemd.services.home-assistant.preStart = ( let # TODO: this probably does not work anymore onboarding = pkgs.writeText "onboarding" '' { "version": 4, "minor_version": 1, "key": "onboarding", "data": { "done": [ ${lib.optionalString cfg.ldap.enable ''"user",''} "core_config", "analytics" ] } } ''; storage = "${config.services.home-assistant.configDir}"; file = "${storage}/.storage/onboarding"; in '' if [ ! -f ${file} ]; then mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file} fi '' ) + (shb.replaceSecrets { userConfig = cfg.config; resultPath = "${config.services.home-assistant.configDir}/secrets.yaml"; generator = shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { }); }); systemd.tmpfiles.rules = [ "f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass" "f ${config.services.home-assistant.configDir}/scenes.yaml 0755 hass hass" "f ${config.services.home-assistant.configDir}/scripts.yaml 0755 hass hass" "d /var/lib/hass/backups 0750 hass hass" ]; }; }