···
9
+
cfg = config.services.homebridge;
11
+
restartCommand = "sudo -n systemctl restart homebridge";
13
+
defaultConfigUIPlatform = {
14
+
inherit (cfg.uiSettings)
24
+
description = "Homebridge";
26
+
inherit (cfg.settings.bridge) name port;
27
+
# These have to be set at least once, otherwise the homebridge will not work
28
+
username = "CC:22:3D:E3:CE:30";
32
+
defaultConfigUIPlatform
36
+
defaultConfigFile = settingsFormat.generate "config.json" defaultConfig;
38
+
nixOverrideConfig = cfg.settings // {
39
+
platforms = [ cfg.uiSettings ] ++ cfg.settings.platforms;
42
+
nixOverrideConfigFile = settingsFormat.generate "nixOverrideConfig.json" nixOverrideConfig;
44
+
# Create a single jq filter that updates all fields at once
45
+
# Platforms need to be unique by "platform"
46
+
# Accessories need to be unique by "name"
48
+
reduce .[] as $item (
52
+
((.platforms // []) + ($item.platforms // [])) |
53
+
group_by(.platform) |
54
+
map(reduce .[] as $platform ({}; . * $platform))
57
+
((.accessories // []) + ($item.accessories // [])) |
59
+
map(reduce .[] as $accessory ({}; . * $accessory))
65
+
jqMergeFilterFile = pkgs.writeTextFile {
66
+
name = "jqMergeFilter.jq";
67
+
text = jqMergeFilter;
70
+
# Validation function to ensure no platform has the platform "config".
71
+
# We want to make sure settings for the "config" platform are set in uiSettings.
75
+
conflictingPlatforms = builtins.filter (p: p.platform == "config") platforms;
77
+
if builtins.length conflictingPlatforms > 0 then
78
+
throw "The platforms list must not contain any platform with platform type 'config'. Use the uiSettings attribute instead."
82
+
settingsFormat = pkgs.formats.json { };
85
+
options.services.homebridge = with lib.types; {
89
+
# services.homebridge = {
91
+
# # Necessary for service to be reachable
92
+
# openFirewall = true;
96
+
enable = lib.mkEnableOption "Homebridge: Homekit home automation";
98
+
user = lib.mkOption {
100
+
default = "homebridge";
101
+
description = "User to run homebridge as.";
104
+
group = lib.mkOption {
106
+
default = "homebridge";
107
+
description = "Group to run homebridge as.";
110
+
openFirewall = lib.mkEnableOption "" // {
112
+
Open ports in the firewall for the Homebridge web interface and service.
116
+
userStoragePath = lib.mkOption {
118
+
default = "/var/lib/homebridge";
120
+
Path to store homebridge user files (needs to be writeable).
124
+
pluginPath = lib.mkOption {
126
+
default = "/var/lib/homebridge/node_modules";
128
+
Path to the plugin download directory (needs to be writeable).
129
+
Seems this needs to end with node_modules, as Homebridge will run npm
130
+
on the parent directory.
134
+
environmentFile = lib.mkOption {
135
+
type = types.nullOr types.str;
138
+
Path to an environment-file which may contain secrets.
142
+
settings = lib.mkOption {
145
+
Configuration options for homebridge.
147
+
For more details, see [the homebridge documentation](https://github.com/homebridge/homebridge/wiki/Homebridge-Config-JSON-Explained).
150
+
freeformType = settingsFormat.type;
152
+
description = lib.mkOption {
154
+
default = "Homebridge";
155
+
description = "Description of the homebridge instance.";
159
+
bridge.name = lib.mkOption {
161
+
default = "Homebridge";
162
+
description = "Name of the homebridge";
165
+
bridge.port = lib.mkOption {
168
+
description = "The port homebridge listens on";
171
+
platforms = lib.mkOption {
172
+
description = "Homebridge Platforms";
174
+
apply = validatePlatforms;
175
+
type = listOf (submodule {
176
+
freeformType = settingsFormat.type;
178
+
name = lib.mkOption {
180
+
description = "Name of the platform";
182
+
platform = lib.mkOption {
184
+
description = "Platform type";
190
+
accessories = lib.mkOption {
191
+
description = "Homebridge Accessories";
193
+
type = listOf (submodule {
194
+
freeformType = settingsFormat.type;
196
+
name = lib.mkOption {
198
+
description = "Name of the accessory";
200
+
accessory = lib.mkOption {
202
+
description = "Accessory type";
211
+
# Defines the parameters for the Homebridge UI Plugin.
212
+
# This submodule will get merged into the "platforms" array
214
+
uiSettings = lib.mkOption {
215
+
# Full list of UI settings can be found here: https://github.com/homebridge/homebridge-config-ui-x/wiki/Config-Options
218
+
Configuration options for homebridge config UI plugin.
220
+
For more details, see [the homebridge-config-ui-x documentation](https://github.com/homebridge/homebridge-config-ui-x/wiki/Config-Options).
223
+
freeformType = settingsFormat.type;
225
+
## Following parameters must be set, and can't be changed.
227
+
# Must be "config" for UI service to see its config
228
+
platform = lib.mkOption {
230
+
default = "config";
231
+
description = "Type of the homebridge UI platform";
235
+
name = lib.mkOption {
237
+
default = "Config";
238
+
description = "Name of the homebridge UI platform";
242
+
# Homebridge can be installed many ways, but we're forcing a double service systemd setup
243
+
# This command will restart both services
244
+
restart = lib.mkOption {
246
+
default = restartCommand;
247
+
description = "Command to restart the homebridge UI service";
251
+
# We're using systemd, so make sure logs is setup to pull from systemd
252
+
log.method = lib.mkOption {
254
+
default = "systemd";
255
+
description = "Method to use for logging";
259
+
log.service = lib.mkOption {
261
+
default = "homebridge";
262
+
description = "Name of the systemd service to log to";
266
+
# The following options are allowed to be changed.
267
+
port = lib.mkOption {
270
+
description = "The port the UI web service should listen on";
277
+
config = lib.mkIf cfg.enable {
278
+
systemd.services.homebridge = {
279
+
description = "Homebridge";
280
+
wants = [ "network-online.target" ];
283
+
"network-online.target"
285
+
wantedBy = [ "multi-user.target" ];
287
+
# On start, if the config file is missing, create a default one
288
+
# Otherwise, ensure that the config file is using the
289
+
# properties as specified by nix.
290
+
# Not sure if there is a better way to do this than to use jq
291
+
# to replace sections of json.
293
+
# If the user storage path does not exist, create it
294
+
if [ ! -d "${cfg.userStoragePath}" ]; then
295
+
install -d -m 700 -o ${cfg.user} -g ${cfg.group} "${cfg.userStoragePath}"
297
+
# If there is no config file, create a placeholder default
298
+
if [ ! -e "${cfg.userStoragePath}/config.json" ]; then
299
+
install -D -m 600 -o ${cfg.user} -g ${cfg.group} "${defaultConfigFile}" "${cfg.userStoragePath}/config.json"
302
+
# Apply all nix override settings to config.json in a single jq operation
303
+
${pkgs.jq}/bin/jq -s -f "${jqMergeFilterFile}" "${cfg.userStoragePath}/config.json" "${nixOverrideConfigFile}" | ${pkgs.jq}/bin/jq . > "${cfg.userStoragePath}/config.json.tmp"
304
+
install -D -m 600 -o ${cfg.user} -g ${cfg.group} "${cfg.userStoragePath}/config.json.tmp" "${cfg.userStoragePath}/config.json"
306
+
# Remove temporary files
307
+
rm "${cfg.userStoragePath}/config.json.tmp"
309
+
# Make sure plugin directory exists
310
+
install -d -m 755 -o ${cfg.user} -g ${cfg.group} "${cfg.pluginPath}"
312
+
# In order for hb-service to detect the homebridge installation, we need to create a folder structure
313
+
# where homebridge and homebrdige-config-ui-x node modules are side by side, and then point
314
+
# UIX_BASE_PATH_OVERRIDE at the homebridge-config-ui-x node module in the service environment.
315
+
# So, first create a directory to symlink these packages to
316
+
install -d -m 755 -o ${cfg.user} -g ${cfg.group} "${cfg.userStoragePath}/homebridge-packages"
318
+
# Then, symlink in the homebridge and homebridge-config-ui-x packages
319
+
rm -rf "${cfg.userStoragePath}/homebridge-packages/homebridge"
320
+
ln -s "${pkgs.homebridge}/lib/node_modules/homebridge" "${cfg.userStoragePath}/homebridge-packages/homebridge"
321
+
rm -rf "${cfg.userStoragePath}/homebridge-packages/homebridge-config-ui-x"
322
+
ln -s "${pkgs.homebridge-config-ui-x}/lib/node_modules/homebridge-config-ui-x" "${cfg.userStoragePath}/homebridge-packages/homebridge-config-ui-x"
325
+
# hb-service environment variables based on source code analysis
327
+
HOMEBRIDGE_CONFIG_UI_TERMINAL = "1";
328
+
DISABLE_OPENCOLLECTIVE = "true";
329
+
# Required or homebridge will search the global npm namespace
330
+
UIX_STRICT_PLUGIN_RESOLUTION = "1";
331
+
# Workaround to ensure homebridge does not run in sudo mode
332
+
HOMEBRIDGE_APT_PACKAGE = "1";
333
+
# Required to get the service to detect the homebridge install correctly
334
+
UIX_BASE_PATH_OVERRIDE = "${cfg.userStoragePath}/homebridge-packages/homebridge-config-ui-x";
337
+
path = with pkgs; [
338
+
# Tools listed in homebridge's installation documentations:
339
+
# https://github.com/homebridge/homebridge/wiki/Install-Homebridge-on-Arch-Linux
344
+
# Required for access to systemctl and journalctl
346
+
# Required for access to sudo
348
+
# Some plugins need bash to download tools
352
+
# Settings from https://github.com/homebridge/homebridge-config-ui-x/blob/latest/src/bin/platforms/linux.ts
356
+
PermissionsStartOnly = true;
357
+
StateDirectory = "homebridge";
358
+
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
359
+
ExecStart = "${pkgs.homebridge-config-ui-x}/bin/hb-service run -U ${cfg.userStoragePath} -P ${cfg.pluginPath}";
360
+
Restart = "always";
362
+
KillMode = "process";
363
+
CapabilityBoundingSet = [
366
+
"CAP_NET_BIND_SERVICE"
377
+
AmbientCapabilities = [
379
+
"CAP_NET_BIND_SERVICE"
384
+
# Create a user whose home folder is the user storage path
385
+
users.users = lib.mkIf (cfg.user == "homebridge") {
387
+
inherit (cfg) group;
388
+
# Necessary so that this user can run journalctl
389
+
extraGroups = [ "systemd-journal" ];
390
+
description = "homebridge user";
391
+
isSystemUser = true;
392
+
home = cfg.userStoragePath;
396
+
users.groups = lib.mkIf (cfg.group == "homebridge") {
400
+
# Need passwordless sudo for a few commands
401
+
# homebridge-config-ui-x needs for some features
402
+
security.sudo.extraRules = [
404
+
users = [ cfg.user ];
407
+
# Ability to restart homebridge service
408
+
command = "${pkgs.systemd}/bin/systemctl restart homebridge";
409
+
options = [ "NOPASSWD" ];
412
+
# Ability to shutdown server
413
+
command = "${pkgs.systemd}/bin/shutdown -h now";
414
+
options = [ "NOPASSWD" ];
417
+
# Ability to restart server
418
+
command = "${pkgs.systemd}/bin/shutdown -r now";
419
+
options = [ "NOPASSWD" ];
425
+
networking.firewall = {
426
+
allowedTCPPorts = lib.mkIf cfg.openFirewall [
427
+
cfg.settings.bridge.port
428
+
cfg.uiSettings.port
430
+
allowedUDPPorts = lib.mkIf cfg.openFirewall [ 5353 ];