From e0b4ab1a31c36dc6459b01cb882fe266206388b7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 22 Mar 2024 05:19:14 +0100 Subject: [PATCH] nixos/wyoming/satellite: init --- .../manual/release-notes/rl-2405.section.md | 2 + nixos/modules/module-list.nix | 1 + .../home-automation/wyoming/satellite.nix | 244 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 nixos/modules/services/home-automation/wyoming/satellite.nix diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 01ba9038fa75..ea89ccff41c7 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -126,6 +126,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [armagetronad](https://wiki.armagetronad.org), a mid-2000s 3D lightcycle game widely played at iD Tech Camps. You can define multiple servers using `services.armagetronad..enable`. +- [wyoming-satellite](https://github.com/rhasspy/wyoming-satellite), a voice assistant satellite for Home Assistant using the Wyoming protocol. Available as [services.wyoming.satellite]($opt-services.wyoming.satellite.enable). + - [TuxClocker](https://github.com/Lurkki14/tuxclocker), a hardware control and monitoring program. Available as [programs.tuxclocker](#opt-programs.tuxclocker.enable). - [ALVR](https://github.com/alvr-org/alvr), a VR desktop streamer. Available as [programs.alvr](#opt-programs.alvr.enable) diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 8db2e737c8e5..0022e6ff7908 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -588,6 +588,7 @@ ./services/home-automation/govee2mqtt.nix ./services/home-automation/home-assistant.nix ./services/home-automation/matter-server.nix + ./services/home-automation/wyoming/satellite.nix ./services/home-automation/zigbee2mqtt.nix ./services/home-automation/zwave-js.nix ./services/logging/SystemdJournal2Gelf.nix diff --git a/nixos/modules/services/home-automation/wyoming/satellite.nix b/nixos/modules/services/home-automation/wyoming/satellite.nix new file mode 100644 index 000000000000..531d375e703a --- /dev/null +++ b/nixos/modules/services/home-automation/wyoming/satellite.nix @@ -0,0 +1,244 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.wyoming.satellite; + + inherit (lib) + elem + escapeShellArgs + getExe + literalExpression + mkOption + mkEnableOption + mkIf + mkPackageOption + optional + optionals + types + ; + + finalPackage = cfg.package.overridePythonAttrs (oldAttrs: { + propagatedBuildInputs = oldAttrs.propagatedBuildInputs + # for audio enhancements like auto-gain, noise suppression + ++ cfg.package.optional-dependencies.webrtc + # vad is currently optional, because it is broken on aarch64-linux + ++ optionals cfg.vad.enable cfg.package.optional-dependencies.silerovad; + }); +in + +{ + meta.buildDocsInSandbox = false; + + options.services.wyoming.satellite = with types; { + enable = mkEnableOption "Wyoming Satellite"; + + package = mkPackageOption pkgs "wyoming-satellite" { }; + + user = mkOption { + type = str; + example = "alice"; + description = '' + User to run wyoming-satellite under. + ''; + }; + + group = mkOption { + type = str; + default = "users"; + description = '' + Group to run wyoming-satellite under. + ''; + }; + + uri = mkOption { + type = str; + default = "tcp://0.0.0.0:10700"; + description = '' + URI where wyoming-satellite will bind its socket. + ''; + }; + + name = mkOption { + type = str; + default = config.networking.hostName; + defaultText = literalExpression '' + config.networking.hostName + ''; + description = '' + Name of the satellite. + ''; + }; + + area = mkOption { + type = nullOr str; + default = null; + example = "Kitchen"; + description = '' + Area to the satellite. + ''; + }; + + microphone = { + command = mkOption { + type = str; + default = "arecord -r 16000 -c 1 -f S16_LE -t raw"; + description = '' + Program to run for audio input. + ''; + }; + + autoGain = mkOption { + type = ints.between 0 31; + default = 5; + example = 15; + description = '' + Automatic gain control in dbFS, with 31 being the loudest value. Set to 0 to disable. + ''; + }; + + noiseSuppression = mkOption { + type = ints.between 0 4; + default = 2; + example = 3; + description = '' + Noise suppression level with 4 being the maximum suppression, + which may cause audio distortion. Set to 0 to disable. + ''; + }; + }; + + sound = { + command = mkOption { + type = nullOr str; + default = "aplay -r 22050 -c 1 -f S16_LE -t raw"; + description = '' + Program to run for sound output. + ''; + }; + }; + + sounds = { + awake = mkOption { + type = nullOr path; + default = null; + description = '' + Path to audio file in WAV format to play when wake word is detected. + ''; + }; + + done = mkOption { + type = nullOr path; + default = null; + description = '' + Path to audio file in WAV format to play when voice command recording has ended. + ''; + }; + }; + + vad = { + enable = mkOption { + type = bool; + default = true; + description = '' + Whether to enable voice activity detection. + + Enabling will result in only streaming audio, when speech gets + detected. + ''; + }; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = '' + Extra arguments to pass to the executable. + + Check `wyoming-satellite --help` for possible options. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services."wyoming-satellite" = { + description = "Wyoming Satellite"; + after = [ + "network-online.target" + "sound.target" + ]; + wants = [ + "network-online.target" + "sound.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + path = with pkgs; [ + alsa-utils + ]; + script = let + optionalParam = param: argument: optionals (!elem argument [ null 0 false ]) [ + param argument + ]; + in '' + export XDG_RUNTIME_DIR=/run/user/$UID + ${escapeShellArgs ([ + (getExe finalPackage) + "--uri" cfg.uri + "--name" cfg.name + "--mic-command" cfg.microphone.command + ] + ++ optionalParam "--mic-auto-gain" cfg.microphone.autoGain + ++ optionalParam "--mic-noise-suppression" cfg.microphone.noiseSuppression + ++ optionalParam "--area" cfg.area + ++ optionalParam "--snd-command" cfg.sound.command + ++ optionalParam "--awake-wav" cfg.sounds.awake + ++ optionalParam "--done-wav" cfg.sounds.done + ++ optional cfg.vad.enable "--vad" + ++ cfg.extraArgs)} + ''; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + # https://github.com/rhasspy/hassio-addons/blob/master/assist_microphone/rootfs/etc/s6-overlay/s6-rc.d/assist_microphone/run + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = false; # onnxruntime/capi/onnxruntime_pybind11_state.so: cannot enable executable stack as shared object requires: Operation not permitted + PrivateDevices = true; + PrivateUsers = true; + ProtectHome = false; # Would deny access to local pulse/pipewire server + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "all"; # Error in cpuinfo: failed to parse processor information from /proc/cpuinfo + Restart = "always"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SupplementaryGroups = [ + "audio" + ]; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + }; +}