From 710428c5a02a1b6c0789281c493e9555a16e4d22 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Mon, 29 Apr 2024 00:11:51 -0700 Subject: [PATCH] add voice option to home-assistant --- flake.lock | 6 +- flake.nix | 1 + lib/default.nix | 7 +- modules/services/home-assistant.nix | 52 ++++++- test/vm/home-assistant.nix | 219 ++++++++++++++++++++++++++++ 5 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 test/vm/home-assistant.nix diff --git a/flake.lock b/flake.lock index f1194f2..8235dfe 100644 --- a/flake.lock +++ b/flake.lock @@ -35,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1714076141, - "narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=", + "lastModified": 1714253743, + "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856", + "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cdb503b..3f88bd3 100644 --- a/flake.nix +++ b/flake.nix @@ -111,6 +111,7 @@ // (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix) // (vm_test "authelia" ./test/vm/authelia.nix) // (vm_test "grocy" ./test/vm/grocy.nix) + // (vm_test "home-assistant" ./test/vm/home-assistant.nix) // (vm_test "jellyfin" ./test/vm/jellyfin.nix) // (vm_test "ldap" ./test/vm/ldap.nix) // (vm_test "lib" ./test/vm/lib.nix) diff --git a/lib/default.nix b/lib/default.nix index 0d26ebf..8ff3515 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -29,12 +29,11 @@ rec { mkdir -p $(dirname ${templatePath}) ln -fs ${file} ${templatePath} rm -f ${resultPath} - if [ -z "${sedPatterns}" ]; then + '' + (if sedPatterns == "" then '' cat ${templatePath} > ${resultPath} - else + '' else '' ${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath} > ${resultPath} - fi - ''; + ''); secretFileType = lib.types.submodule { options = { diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix index 4593469..c708ed9 100644 --- a/modules/services/home-assistant.nix +++ b/modules/services/home-assistant.nix @@ -136,6 +136,42 @@ in }; }; + 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; }; + }; + }; + }; + }; + backupCfg = lib.mkOption { type = lib.types.anything; description = "Backup configuration for home-assistant"; @@ -258,6 +294,10 @@ in }; }; + 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; @@ -274,7 +314,7 @@ in }; }; - systemd.services.home-assistant.preStart = lib.mkIf cfg.ldap.enable ( + systemd.services.home-assistant.preStart = lib.mkIf cfg.enable ( let onboarding = pkgs.writeText "onboarding" '' { @@ -289,16 +329,16 @@ in } } ''; - storage = "${config.services.home-assistant.configDir}"; - file = "${storage}/.storage/onboarding"; + configDir = "${config.services.home-assistant.configDir}"; + file = "${configDir}/.storage/onboarding"; in '' - if ! -f ${file}; then - mkdir -p ${storage} && cp ${onboarding} ${file} + if ! [ -f ${file} ]; then + mkdir -p ${configDir}/.storage && cp ${onboarding} ${file} fi '' + shblib.replaceSecrets { userConfig = cfg.config; - resultPath = "${config.services.home-assistant.configDir}/secrets.yaml"; + resultPath = "${configDir}/secrets.yaml"; generator = name: value: lib.generators.toYAML {} value; }); diff --git a/test/vm/home-assistant.nix b/test/vm/home-assistant.nix new file mode 100644 index 0000000..ff343c0 --- /dev/null +++ b/test/vm/home-assistant.nix @@ -0,0 +1,219 @@ +{ pkgs, lib, ... }: +let + pkgs' = pkgs; + + commonTestScript = { nodes, extraPorts ? [], ... }: + let + hasSSL = !(isNull nodes.server.shb.home-assistant.ssl); + fqdn = if hasSSL then "https://ha.example.com" else "http://ha.example.com"; + in + '' + import json + import os + import pathlib + + start_all() + server.wait_for_unit("home-assistant.service") + server.wait_for_open_port(${toString nodes.server.services.home-assistant.config.http.server_port}) + '' + + lib.concatMapStringsSep "\n" (port: '' + server.wait_for_open_port(${toString port}) + '') extraPorts + + '' + + if ${if hasSSL then "True" else "False"}: + server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt") + client.succeed("rm -r /etc/ssl/certs") + client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt") + + def curl(target, format, endpoint, succeed=True): + return json.loads(target.succeed( + "curl --fail-with-body --silent --show-error --output /dev/null --location" + + " --connect-to ha.example.com:443:server:443" + + " --connect-to ha.example.com:80:server:80" + + f" --write-out '{format}'" + + " " + endpoint + )) + + print(server.succeed("cat /var/lib/hass/configuration.yaml")) + print(server.succeed("systemctl cat home-assistant")) + print(server.succeed("cat ''$(systemctl cat home-assistant | grep ExecStartPre | cut -d= -f2)")) + print(server.succeed("cat /var/lib/hass/secrets.yaml.template")) + print(server.succeed("cat /var/lib/hass/secrets.yaml")) + + with subtest("access"): + response = curl(client, """{"code":%{response_code}}""", "${fqdn}") + + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + ''; + + modules = { + base = { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/nginx.nix + ../../modules/services/home-assistant.nix + ]; + + # Nginx port. + networking.firewall.allowedTCPPorts = [ 80 ]; + # VM needs a bit more memory than default. + # virtualisation.memorySize = 4096; + }; + + basic = { + imports = [ + { + options = { + shb.backup = lib.mkOption { type = lib.types.anything; }; + shb.authelia = lib.mkOption { type = lib.types.anything; }; + }; + } + ]; + + shb.home-assistant = { + enable = true; + domain = "example.com"; + subdomain = "ha"; + + config = { + name = "SHB Test"; + country = "BE"; # https://en.wikipedia.org/wiki/ISO_3166-1 + latitude = "0"; + longitude = "0"; + time_zone.source = pkgs.writeText "timeZoneSecret" "UTC"; # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + unit_system = "metric"; + }; + }; + }; + + ldap = { config, ... }: { + imports = [ + ../../modules/blocks/ldap.nix + ]; + + shb.ldap = { + enable = true; + domain = "example.com"; + subdomain = "ldap"; + ldapPort = 3890; + webUIListenPort = 17170; + dcdomain = "dc=example,dc=com"; + ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword"; + jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret"; + }; + + shb.home-assistant = { + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.ldap.ldapPort; + userGroup = "homeassistant_user"; + }; + }; + }; + + voice = { + shb.home-assistant.voice.text-to-speech = { + "fr" = { + enable = true; + voice = "fr-siwis-medium"; + uri = "tcp://0.0.0.0:10200"; + speaker = 0; + }; + "en" = { + enable = true; + voice = "en_GB-alba-medium"; + uri = "tcp://0.0.0.0:10201"; + speaker = 0; + }; + }; + shb.home-assistant.voice.speech-to-text = { + "tiny-fr" = { + enable = true; + model = "base-int8"; + language = "fr"; + uri = "tcp://0.0.0.0:10300"; + device = "cpu"; + }; + "tiny-en" = { + enable = true; + model = "base-int8"; + language = "en"; + uri = "tcp://0.0.0.0:10301"; + device = "cpu"; + }; + }; + shb.home-assistant.voice.wakeword = { + enable = true; + uri = "tcp://127.0.0.1:10400"; + preloadModels = [ + "alexa" + "hey_jarvis" + "hey_mycroft" + "hey_rhasspy" + "ok_nabu" + ]; + }; + }; + }; +in +{ + basic = pkgs.testers.runNixOSTest { + name = "home-assistant-basic"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + modules.base + modules.basic + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; + + ldap = pkgs.testers.runNixOSTest { + name = "home-assistant-ldap"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + modules.base + modules.basic + modules.ldap + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; + + voice = pkgs.testers.runNixOSTest { + name = "home-assistant-basic"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + modules.base + modules.basic + modules.voice + ]; + }; + + nodes.client = {}; + + testScript = { nodes, ... }: commonTestScript { + inherit nodes; + extraPorts = [ + 10200 + 10201 + # 10300 + # 10301 + 10400 + ]; + }; + }; +}