1{ config
2, lib
3, pkgs
4, ...
5}:
6
7let
8 cfg = config.services.homeassistant-satellite;
9
10 inherit (lib)
11 escapeShellArg
12 escapeShellArgs
13 mkOption
14 mdDoc
15 mkEnableOption
16 mkIf
17 mkPackageOptionMD
18 types
19 ;
20
21 inherit (builtins)
22 toString
23 ;
24
25 # override the package with the relevant vad dependencies
26 package = cfg.package.overridePythonAttrs (oldAttrs: {
27 propagatedBuildInputs = oldAttrs.propagatedBuildInputs
28 ++ lib.optional (cfg.vad == "webrtcvad") cfg.package.optional-dependencies.webrtc
29 ++ lib.optional (cfg.vad == "silero") cfg.package.optional-dependencies.silerovad
30 ++ lib.optional (cfg.pulseaudio.enable) cfg.package.optional-dependencies.pulseaudio;
31 });
32
33in
34
35{
36 meta.buildDocsInSandbox = false;
37
38 options.services.homeassistant-satellite = with types; {
39 enable = mkEnableOption (mdDoc "Home Assistant Satellite");
40
41 package = mkPackageOptionMD pkgs "homeassistant-satellite" { };
42
43 user = mkOption {
44 type = str;
45 example = "alice";
46 description = mdDoc ''
47 User to run homeassistant-satellite under.
48 '';
49 };
50
51 group = mkOption {
52 type = str;
53 default = "users";
54 description = mdDoc ''
55 Group to run homeassistant-satellite under.
56 '';
57 };
58
59 host = mkOption {
60 type = str;
61 example = "home-assistant.local";
62 description = mdDoc ''
63 Hostname on which your Home Assistant instance can be reached.
64 '';
65 };
66
67 port = mkOption {
68 type = port;
69 example = 8123;
70 description = mdDoc ''
71 Port on which your Home Assistance can be reached.
72 '';
73 apply = toString;
74 };
75
76 protocol = mkOption {
77 type = enum [ "http" "https" ];
78 default = "http";
79 example = "https";
80 description = mdDoc ''
81 The transport protocol used to connect to Home Assistant.
82 '';
83 };
84
85 tokenFile = mkOption {
86 type = path;
87 example = "/run/keys/hass-token";
88 description = mdDoc ''
89 Path to a file containing a long-lived access token for your Home Assistant instance.
90 '';
91 apply = escapeShellArg;
92 };
93
94 sounds = {
95 awake = mkOption {
96 type = nullOr str;
97 default = null;
98 description = mdDoc ''
99 Audio file to play when the wake word is detected.
100 '';
101 };
102
103 done = mkOption {
104 type = nullOr str;
105 default = null;
106 description = mdDoc ''
107 Audio file to play when the voice command is done.
108 '';
109 };
110 };
111
112 vad = mkOption {
113 type = enum [ "disabled" "webrtcvad" "silero" ];
114 default = "disabled";
115 example = "silero";
116 description = mdDoc ''
117 Voice activity detection model. With `disabled` sound will be transmitted continously.
118 '';
119 };
120
121 pulseaudio = {
122 enable = mkEnableOption "recording/playback via PulseAudio or PipeWire";
123
124 socket = mkOption {
125 type = nullOr str;
126 default = null;
127 example = "/run/user/1000/pulse/native";
128 description = mdDoc ''
129 Path or hostname to connect with the PulseAudio server.
130 '';
131 };
132
133 duckingVolume = mkOption {
134 type = nullOr float;
135 default = null;
136 example = 0.4;
137 description = mdDoc ''
138 Reduce output volume (between 0 and 1) to this percentage value while recording.
139 '';
140 };
141
142 echoCancellation = mkEnableOption "acoustic echo cancellation";
143 };
144
145 extraArgs = mkOption {
146 type = listOf str;
147 default = [ ];
148 description = mdDoc ''
149 Extra arguments to pass to the commandline.
150 '';
151 apply = escapeShellArgs;
152 };
153 };
154
155 config = mkIf cfg.enable {
156 systemd.services."homeassistant-satellite" = {
157 description = "Home Assistant Satellite";
158 after = [
159 "network-online.target"
160 ];
161 wants = [
162 "network-online.target"
163 ];
164 wantedBy = [
165 "multi-user.target"
166 ];
167 path = with pkgs; [
168 ffmpeg-headless
169 ] ++ lib.optionals (!cfg.pulseaudio.enable) [
170 alsa-utils
171 ];
172 serviceConfig = {
173 User = cfg.user;
174 Group = cfg.group;
175 # https://github.com/rhasspy/hassio-addons/blob/master/assist_microphone/rootfs/etc/s6-overlay/s6-rc.d/assist_microphone/run
176 ExecStart = ''
177 ${package}/bin/homeassistant-satellite \
178 --host ${cfg.host} \
179 --port ${cfg.port} \
180 --protocol ${cfg.protocol} \
181 --token-file ${cfg.tokenFile} \
182 --vad ${cfg.vad} \
183 ${lib.optionalString cfg.pulseaudio.enable "--pulseaudio"}${lib.optionalString (cfg.pulseaudio.socket != null) "=${cfg.pulseaudio.socket}"} \
184 ${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.duckingVolume != null) "--ducking-volume=${toString cfg.pulseaudio.duckingVolume}"} \
185 ${lib.optionalString (cfg.pulseaudio.enable && cfg.pulseaudio.echoCancellation) "--echo-cancel"} \
186 ${lib.optionalString (cfg.sounds.awake != null) "--awake-sound=${toString cfg.sounds.awake}"} \
187 ${lib.optionalString (cfg.sounds.done != null) "--done-sound=${toString cfg.sounds.done}"} \
188 ${cfg.extraArgs}
189 '';
190 CapabilityBoundingSet = "";
191 DeviceAllow = "";
192 DevicePolicy = "closed";
193 LockPersonality = true;
194 MemoryDenyWriteExecute = false; # onnxruntime/capi/onnxruntime_pybind11_state.so: cannot enable executable stack as shared object requires: Operation not permitted
195 PrivateDevices = true;
196 PrivateUsers = true;
197 ProtectHome = false; # Would deny access to local pulse/pipewire server
198 ProtectHostname = true;
199 ProtectKernelLogs = true;
200 ProtectKernelModules = true;
201 ProtectKernelTunables = true;
202 ProtectControlGroups = true;
203 ProtectProc = "invisible";
204 ProcSubset = "all"; # Error in cpuinfo: failed to parse processor information from /proc/cpuinfo
205 Restart = "always";
206 RestrictAddressFamilies = [
207 "AF_INET"
208 "AF_INET6"
209 "AF_UNIX"
210 ];
211 RestrictNamespaces = true;
212 RestrictRealtime = true;
213 SupplementaryGroups = [
214 "audio"
215 ];
216 SystemCallArchitectures = "native";
217 SystemCallFilter = [
218 "@system-service"
219 "~@privileged"
220 ];
221 UMask = "0077";
222 };
223 };
224 };
225}