Personal Nix setup
at main 7.7 kB view raw
1{ lib, config, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.modules.automation; 6 7 format = pkgs.formats.json { }; 8 configFile = format.generate "zigbee2mqtt.yaml" cfg.homebridge.settings; 9 10 packageJson = builtins.fromJSON (builtins.readFile ./package.json); 11 pluginsDefault = lists.remove "homebridge" (attrNames packageJson.dependencies); 12 13 homebridge-distribution = pkgs.stdenvNoCC.mkDerivation rec { 14 name = "homebridge-distribution"; 15 src = ./.; 16 offlineCache = pkgs.fetchYarnDeps { 17 yarnLock = src + "/yarn.lock"; 18 hash = "sha256-a4zkEr/v6ZBTXS6Q5oij5G0zsGh1QCGa8/5Do0C/inM="; 19 }; 20 strictDeps = true; 21 nativeBuildInputs = with pkgs; [ 22 yarnConfigHook 23 yarnInstallHook 24 nodejs 25 ]; 26 }; 27 28 defaultConfigPlatform = { 29 platform = "config"; 30 name = "Config"; 31 auth = "none"; 32 port = cfg.homebridge.frontend.port; 33 disableServerMetricsMonitoring = true; 34 homebridgePackagePath = "${cfg.homebridge.userStoragePath}/node_modules/homebridge"; 35 standalone = true; 36 sudo = false; 37 log = { 38 method = "systemd"; 39 service = "homebridge"; 40 }; 41 }; 42 43 defaultServiceConfig = { 44 Type = "simple"; 45 User = "homebridge"; 46 Group = "homebridge"; 47 PermissionsStartOnly = true; 48 WorkingDirectory = cfg.homebridge.userStoragePath; 49 Restart = "always"; 50 RestartSec = 3; 51 KillMode = "process"; 52 CapabilityBoundingSet = [ "CAP_IPC_LOCK" "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" "CAP_SETGID" "CAP_SETUID" "CAP_SYS_CHROOT" "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_AUDIT_WRITE" "CAP_SYS_ADMIN" ]; 53 AmbientCapabilities = [ "CAP_NET_RAW" "CAP_NET_BIND_SERVICE" ]; 54 }; 55 56 defaultArgs = [ 57 "-U ${cfg.homebridge.userStoragePath}" 58 "-P ${cfg.homebridge.pluginPath}" 59 "--strict-plugin-resolution" 60 ] ++ optionals cfg.homebridge.frontend.enable ["-I"]; 61 62 frontendType = types.submodule { 63 options = { 64 enable = mkOption { 65 default = false; 66 example = true; 67 description = "Whether to enable Homebridge's frontend."; 68 type = types.bool; 69 }; 70 port = mkOption { 71 default = 8125; 72 example = 8125; 73 description = "The port to use for Homebridge's frontend."; 74 type = types.port; 75 }; 76 }; 77 }; 78 79 bridgeType = types.submodule { 80 options = { 81 name = mkOption { 82 default = "Homebridge"; 83 example = "Homebridge name"; 84 type = types.str; 85 }; 86 username = mkOption { 87 default = "CC:22:3D:E3:CE:30"; 88 type = types.str; 89 }; 90 pin = mkOption { 91 default = "031-45-154"; 92 type = types.str; 93 }; 94 port = mkOption { 95 default = 51826; 96 type = types.port; 97 }; 98 advertiser = mkOption { 99 default = if config.services.resolved.enable then "resolved" else "ciao"; 100 type = types.str; 101 }; 102 bind = mkOption { 103 default = lists.remove "lo" config.networking.firewall.trustedInterfaces; 104 type = types.listOf types.str; 105 }; 106 }; 107 }; 108in { 109 options.modules.automation.homebridge = { 110 enable = mkOption { 111 default = false; 112 example = true; 113 description = "Whether to enable Homebridge service."; 114 type = types.bool; 115 }; 116 117 plugins = mkOption { 118 default = pluginsDefault; 119 description = "Names of package installed in the homebridge-distribution."; 120 type = types.listOf types.str; 121 }; 122 123 frontend = mkOption { 124 default = {}; 125 type = frontendType; 126 }; 127 128 bridge = mkOption { 129 default = {}; 130 type = bridgeType; 131 }; 132 133 settings = mkOption { 134 type = format.type; 135 default = { }; 136 }; 137 138 userStoragePath = mkOption { 139 default = "/var/lib/homebridge"; 140 description = "Path to store homebridge user files (needs to be writeable)."; 141 type = types.str; 142 }; 143 144 pluginPath = mkOption { 145 default = "${cfg.homebridge.userStoragePath}/node_modules"; 146 type = types.str; 147 }; 148 }; 149 150 config = mkIf (cfg.enable && cfg.homebridge.enable) { 151 modules.automation.homebridge.settings = { 152 description = mkDefault "Homebridge"; 153 bridge = mkForce cfg.homebridge.bridge; 154 platforms = [defaultConfigPlatform] 155 ++ optionals (cfg.zigbee.enable && cfg.mqtt.enable) [ 156 { 157 platform = "zigbee2mqtt"; 158 mqtt = { 159 base_topic = "zigbee2mqtt"; 160 server = "mqtts://localhost:${toString cfg.mqtt.port}"; 161 disable_qos = true; 162 reject_unauthorized = false; 163 ca = cfg.mqtt.cafile; 164 key = cfg.mqtt.keyfile; 165 cert = cfg.mqtt.certfile; 166 }; 167 defaults.exclude = false; 168 exclude_grouped_devices = false; 169 } 170 ]; 171 }; 172 173 systemd.services.homebridge = { 174 description = "Homebridge"; 175 after = [ "syslog.target" "network.target" ] 176 ++ optionals cfg.mqtt.enable [config.systemd.services.mosquitto.name]; 177 wants = optionals cfg.mqtt.enable [config.systemd.services.mosquitto.name]; 178 wantedBy = [ "multi-user.target" ]; 179 180 serviceConfig = let 181 args = concatStringsSep " " defaultArgs; 182 in defaultServiceConfig // { 183 ExecStart = "${homebridge-distribution}/bin/homebridge ${args}"; 184 }; 185 186 preStart = let 187 inherit (cfg.homebridge) pluginPath userStoragePath plugins; 188 lnPlugins = concatStringsSep "\n" (map 189 (name: '' 190 ln -fns "${homebridge-distribution}/lib/node_modules/homebridge-distribution/node_modules/${name}" "${pluginPath}/${name}" 191 '') 192 plugins); 193 in '' 194 mkdir -p ${pluginPath} 195 ${lnPlugins} 196 cp --no-preserve=mode ${configFile} "${userStoragePath}/config.json" 197 chown homebridge "${userStoragePath}/config.json" "${pluginPath}" 198 chgrp homebridge "${userStoragePath}/config.json" "${pluginPath}" 199 ''; 200 }; 201 202 systemd.services.homebridge-frontend = mkIf cfg.homebridge.frontend.enable { 203 description = "Homebridge Frontend"; 204 after = [ "syslog.target" "network.target" config.systemd.services.homebridge.name ]; 205 requires = [ config.systemd.services.homebridge.name ]; 206 wantedBy = [ "multi-user.target" ]; 207 path = with pkgs; [ 208 nodejs_20 209 nettools 210 gcc 211 gnumake 212 systemd 213 "/run/wrappers" 214 ]; 215 216 environment = { 217 HOMEBRIDGE_CONFIG_UI_TERMINAL = "1"; 218 DISABLE_OPENCOLLECTIVE = "true"; 219 UIX_STRICT_PLUGIN_RESOLUTION = "1"; 220 }; 221 222 serviceConfig = let 223 args = concatStringsSep " " defaultArgs; 224 in defaultServiceConfig // { 225 ExecStart = "${homebridge-distribution}/bin/homebridge-config-ui ${args}"; 226 }; 227 }; 228 229 users = { 230 groups.homebridge = {}; 231 users.homebridge = { 232 home = cfg.homebridge.userStoragePath; 233 createHome = true; 234 group = "homebridge"; 235 extraGroups = [ "systemd-journal" ] ++ optionals cfg.mqtt.enable [config.users.users.mosquitto.name]; 236 isSystemUser = true; 237 }; 238 }; 239 240 security.polkit.extraConfig = optionalString (cfg.homebridge.bridge.advertiser == "resolved") '' 241 // kitten/system: Allow homebridge to register systemd-resolved services 242 // This was enabled via modules.automation.homebridge 243 polkit.addRule(function(action, subject) { 244 if ((action.id == "org.freedesktop.resolve1.register-service" || 245 action.id == "org.freedesktop.resolve1.unregister-service") && 246 subject.user == "${config.users.users.homebridge.name}") { 247 return polkit.Result.YES; 248 } 249 }); 250 ''; 251 }; 252}