{ description = "NixOS configuration for noisebell Pi"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; noisebell.url = "path:./pi-service"; agenix = { url = "github:ryantm/agenix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, noisebell, agenix }: let nixosModule = { config, lib, pkgs, ... }: let cfg = config.services.noisebell; in { options.services.noisebell = { enable = lib.mkEnableOption "noisebell GPIO door monitor"; gpioPin = lib.mkOption { type = lib.types.ints.unsigned; default = 17; description = "GPIO pin number to monitor."; }; debounceSecs = lib.mkOption { type = lib.types.ints.positive; default = 5; description = "Debounce delay in seconds."; }; port = lib.mkOption { type = lib.types.port; default = 8080; description = "HTTP port for the status endpoint."; }; endpointUrl = lib.mkOption { type = lib.types.str; description = "Webhook endpoint URL to POST state changes to."; }; apiKeyFile = lib.mkOption { type = lib.types.path; description = "Path to a file containing the outbound API key for the cache endpoint."; }; inboundApiKeyFile = lib.mkOption { type = lib.types.path; description = "Path to a file containing the inbound API key for authenticating GET requests."; }; retryAttempts = lib.mkOption { type = lib.types.ints.unsigned; default = 3; description = "Number of retries after a failed webhook POST."; }; retryBaseDelaySecs = lib.mkOption { type = lib.types.ints.positive; default = 1; description = "Base delay in seconds for exponential backoff between retries."; }; httpTimeoutSecs = lib.mkOption { type = lib.types.ints.positive; default = 10; description = "Timeout in seconds for outbound HTTP requests to the webhook endpoint."; }; bindAddress = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Address to bind the HTTP server to."; }; activeLow = lib.mkOption { type = lib.types.bool; default = true; description = "Whether a low GPIO level means open. Set to false if your sensor wiring is inverted."; }; restartDelaySecs = lib.mkOption { type = lib.types.ints.positive; default = 5; description = "Seconds to wait before systemd restarts the service on failure."; }; watchdogSecs = lib.mkOption { type = lib.types.ints.positive; default = 30; description = "Watchdog timeout in seconds. The service is restarted if it fails to notify systemd within this interval."; }; }; config = lib.mkIf cfg.enable { users.users.noisebell = { isSystemUser = true; group = "noisebell"; extraGroups = [ "gpio" ]; }; users.groups.noisebell = {}; users.groups.gpio = {}; services.udev.extraRules = '' KERNEL=="gpiomem", GROUP="gpio", MODE="0660" KERNEL=="gpiochip[0-9]*", GROUP="gpio", MODE="0660" ''; systemd.services.noisebell = let bin = "${noisebell.packages.aarch64-linux.default}/bin/noisebell"; in { description = "Noisebell GPIO door monitor"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" "tailscaled.service" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_GPIO_PIN = toString cfg.gpioPin; NOISEBELL_DEBOUNCE_SECS = toString cfg.debounceSecs; NOISEBELL_PORT = toString cfg.port; NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts; NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; NOISEBELL_HTTP_TIMEOUT_SECS = toString cfg.httpTimeoutSecs; NOISEBELL_ENDPOINT_URL = cfg.endpointUrl; NOISEBELL_BIND_ADDRESS = cfg.bindAddress; NOISEBELL_ACTIVE_LOW = if cfg.activeLow then "true" else "false"; NOISEBELL_COMMIT = self.shortRev or "dirty"; RUST_LOG = "info"; }; script = '' export NOISEBELL_API_KEY="$(cat ${cfg.apiKeyFile})" export NOISEBELL_INBOUND_API_KEY="$(cat ${cfg.inboundApiKeyFile})" exec ${bin} ''; serviceConfig = { Type = "notify"; NotifyAccess = "all"; WatchdogSec = cfg.watchdogSecs; Restart = "on-failure"; RestartSec = cfg.restartDelaySecs; User = "noisebell"; Group = "noisebell"; AmbientCapabilities = lib.optionals (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = true; DevicePolicy = "closed"; DeviceAllow = [ "char-gpiomem rw" "char-gpiochip rw" ]; }; }; }; }; in { nixosModules.default = nixosModule; nixosConfigurations.pi = nixpkgs.lib.nixosSystem { system = "aarch64-linux"; modules = [ agenix.nixosModules.default nixosModule ./configuration.nix ./hardware-configuration.nix ]; }; nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { system = "aarch64-linux"; modules = [ ./bootstrap.nix ]; }; devShells.x86_64-linux.default = let pkgs = nixpkgs.legacyPackages.x86_64-linux; in pkgs.mkShell { packages = [ agenix.packages.x86_64-linux.default ]; }; }; }