about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorMartin Weinelt <hexa@darmstadt.ccc.de>2024-03-22 05:19:14 +0100
committerMartin Weinelt <hexa@darmstadt.ccc.de>2024-03-29 03:04:44 +0100
commite0b4ab1a31c36dc6459b01cb882fe266206388b7 (patch)
tree2cf9c3d2c6bfdfb7c754176f56dbb20e83be3d43 /nixos
parenteb4113b79c559bcd99669eb09ca291bae370ea76 (diff)
nixos/wyoming/satellite: init
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/home-automation/wyoming/satellite.nix244
3 files changed, 247 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 01ba9038fa75a..ea89ccff41c73 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.<server>.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 8db2e737c8e5c..0022e6ff79081 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 0000000000000..531d375e703a3
--- /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";
+      };
+    };
+  };
+}