Unverified Commit 410ccec9 authored by Martin Weinelt's avatar Martin Weinelt Committed by GitHub
Browse files

Merge pull request #261931 from mweinelt/homeassistant-satellite

homeassistant-satellite: init at 2.3.0
parents f3a3ac36 95856e52
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -80,6 +80,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).
+1 −0
Original line number Diff line number Diff line
@@ -558,6 +558,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
+1 −1
Original line number Diff line number Diff line
@@ -136,7 +136,7 @@ in
        ProtectKernelTunables = true;
        ProtectControlGroups = true;
        ProtectProc = "invisible";
        ProcSubset = "pid";
        ProcSubset = "all"; # reads /proc/cpuinfo
        RestrictAddressFamilies = [
          "AF_INET"
          "AF_INET6"
+225 −0
Original line number Diff line number Diff line
{ 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";
      };
    };
  };
}
+56 −0
Original line number Diff line number Diff line
{ lib
, python3
, fetchFromGitHub
}:

python3.pkgs.buildPythonApplication rec {
  pname = "homeassistant-satellite";
  version = "2.3.0";
  pyproject = true;

  src = fetchFromGitHub {
    owner = "synesthesiam";
    repo = "homeassistant-satellite";
    rev = "v${version}";
    hash = "sha256-iosutOpkpt0JJIMyALuQSDLj4jk57ITShVyPYlQgMFg=";
  };

  nativeBuildInputs = with python3.pkgs; [
    setuptools
    wheel
  ];

  propagatedBuildInputs = with python3.pkgs; [
    aiohttp
  ];

  passthru.optional-dependencies = {
    pulseaudio = with python3.pkgs; [
      pasimple
      pulsectl
    ];
    silerovad = with python3.pkgs; [
      numpy
      onnxruntime
    ];
    webrtc = with python3.pkgs; [
      webrtc-noise-gain
    ];
  };

  pythonImportsCheck = [
    "homeassistant_satellite"
  ];

  # no tests
  doCheck = false;

  meta = with lib; {
    changelog = "https://github.com/synesthesiam/homeassistant-satellite/blob/v${version}/CHANGELOG.md";
    description = "Streaming audio satellite for Home Assistant";
    homepage = "https://github.com/synesthesiam/homeassistant-satellite";
    license = licenses.mit;
    maintainers = with maintainers; [ hexa ];
    mainProgram = "homeassistant-satellite";
  };
}
Loading