diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index d74dc5b93cdd..bb659b3cc874 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -76,6 +76,8 @@ - [Jool](https://nicmx.github.io/Jool/en/index.html), a kernelspace NAT64 and SIIT implementation, providing translation between IPv4 and IPv6. Available as [networking.jool.enable](#opt-networking.jool.enable). +- [Home Assistant Satellite], a streaming audio satellite for Home Assistant voice pipelines, where you can reuse existing mic/speaker hardware. Available as [services.homeassistant-satellite](#opt-services.homeassistant-satellite.enable). + - [Apache Guacamole](https://guacamole.apache.org/), a cross-platform, clientless remote desktop gateway. Available as [services.guacamole-server](#opt-services.guacamole-server.enable) and [services.guacamole-client](#opt-services.guacamole-client.enable) services. - [pgBouncer](https://www.pgbouncer.org), a PostgreSQL connection pooler. Available as [services.pgbouncer](#opt-services.pgbouncer.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c248aa6f9767..604f53283a15 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -556,6 +556,7 @@ ./services/home-automation/esphome.nix ./services/home-automation/evcc.nix ./services/home-automation/home-assistant.nix + ./services/home-automation/homeassistant-satellite.nix ./services/home-automation/zigbee2mqtt.nix ./services/logging/SystemdJournal2Gelf.nix ./services/logging/awstats.nix diff --git a/nixos/modules/services/home-automation/homeassistant-satellite.nix b/nixos/modules/services/home-automation/homeassistant-satellite.nix new file mode 100644 index 000000000000..e3f0617cf01c --- /dev/null +++ b/nixos/modules/services/home-automation/homeassistant-satellite.nix @@ -0,0 +1,225 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.homeassistant-satellite; + + inherit (lib) + escapeShellArg + escapeShellArgs + mkOption + mdDoc + mkEnableOption + mkIf + mkPackageOptionMD + types + ; + + inherit (builtins) + toString + ; + + # override the package with the relevant vad dependencies + package = cfg.package.overridePythonAttrs (oldAttrs: { + propagatedBuildInputs = oldAttrs.propagatedBuildInputs + ++ lib.optional (cfg.vad == "webrtcvad") cfg.package.optional-dependencies.webrtc + ++ lib.optional (cfg.vad == "silero") cfg.package.optional-dependencies.silerovad + ++ lib.optional (cfg.pulseaudio.enable) cfg.package.optional-dependencies.pulseaudio; + }); + +in + +{ + meta.buildDocsInSandbox = false; + + options.services.homeassistant-satellite = with types; { + enable = mkEnableOption (mdDoc "Home Assistant Satellite"); + + package = mkPackageOptionMD pkgs "homeassistant-satellite" { }; + + user = mkOption { + type = str; + example = "alice"; + description = mdDoc '' + User to run homeassistant-satellite under. + ''; + }; + + group = mkOption { + type = str; + default = "users"; + description = mdDoc '' + Group to run homeassistant-satellite under. + ''; + }; + + host = mkOption { + type = str; + example = "home-assistant.local"; + description = mdDoc '' + Hostname on which your Home Assistant instance can be reached. + ''; + }; + + port = mkOption { + type = port; + example = 8123; + description = mdDoc '' + Port on which your Home Assistance can be reached. + ''; + apply = toString; + }; + + protocol = mkOption { + type = enum [ "http" "https" ]; + default = "http"; + example = "https"; + description = mdDoc '' + The transport protocol used to connect to Home Assistant. + ''; + }; + + tokenFile = mkOption { + type = path; + example = "/run/keys/hass-token"; + description = mdDoc '' + Path to a file containing a long-lived access token for your Home Assistant instance. + ''; + apply = escapeShellArg; + }; + + sounds = { + awake = mkOption { + type = nullOr str; + default = null; + description = mdDoc '' + Audio file to play when the wake word is detected. + ''; + }; + + done = mkOption { + type = nullOr str; + default = null; + description = mdDoc '' + Audio file to play when the voice command is done. + ''; + }; + }; + + vad = mkOption { + type = enum [ "disabled" "webrtcvad" "silero" ]; + default = "disabled"; + example = "silero"; + description = mdDoc '' + Voice activity detection model. With `disabled` sound will be transmitted continously. + ''; + }; + + pulseaudio = { + enable = mkEnableOption "recording/playback via PulseAudio or PipeWire"; + + socket = mkOption { + type = nullOr str; + default = null; + example = "/run/user/1000/pulse/native"; + description = mdDoc '' + Path or hostname to connect with the PulseAudio server. + ''; + }; + + duckingVolume = mkOption { + type = nullOr float; + default = null; + example = 0.4; + description = mdDoc '' + Reduce output volume (between 0 and 1) to this percentage value while recording. + ''; + }; + + echoCancellation = mkEnableOption "acoustic echo cancellation"; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = mdDoc '' + Extra arguments to pass to the commandline. + ''; + apply = escapeShellArgs; + }; + }; + + config = mkIf cfg.enable { + systemd.services."homeassistant-satellite" = { + description = "Home Assistant Satellite"; + after = [ + "network-online.target" + ]; + wants = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + path = with pkgs; [ + ffmpeg-headless + ] ++ lib.optionals (!cfg.pulseaudio.enable) [ + alsa-utils + ]; + 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 + ExecStart = '' + ${package}/bin/homeassistant-satellite \ + --host ${cfg.host} \ + --port ${cfg.port} \ + --protocol ${cfg.protocol} \ + --token-file ${cfg.tokenFile} \ + --vad ${cfg.vad} \ + ${lib.optionalString cfg.pulseaudio.enable "--pulseaudio"}${lib.optionalString (cfg.pulseaudio.socket != null) "=${cfg.pulseaudio.socket}"} \ + ${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.duckingVolume != null) "--ducking-volume=${toString cfg.pulseaudio.duckingVolume}"} \ + ${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.echoCancellation) "--echo-cancel"} \ + ${lib.optionalString (cfg.sounds.awake != null) "--awake-sound=${toString cfg.sounds.awake}"} \ + ${lib.optionalString (cfg.sounds.done != null) "--done-sound=${toString cfg.sounds.done}"} \ + ${cfg.extraArgs} + ''; + 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" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SupplementaryGroups = [ + "audio" + ]; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + }; +}