README.md

Noisebell

Door status monitor for Noisebridge. A Raspberry Pi watches a door sensor and reports open/closed state; remote services cache the data and fan it out to Discord and an Atom feed.

Architecture

1Pi (door sensor) ──webhook──▸ Cache Service ──webhook──▸ Discord Bot
2 │ │
3 polls Pi ◂──┘ Atom feed ◂──┘
4 RSS Service

The Pi runs a small HTTP service that reads GPIO and exposes GET / (status) and GET /info. It also pushes state changes to the cache service via webhook.

The Cache Service is the central hub. It polls the Pi for status and info, stores everything in SQLite, and forwards state changes to downstream webhooks (Discord, etc.). It exposes a read API for consumers.

The Discord Bot receives webhooks from the cache service and posts embeds to a Discord channel.

The RSS Service fetches history from the cache service and serves an Atom feed of door events from the last 7 days.

Services

ServiceCrateDefault PortDescription
Cachenoisebell-cache3000Polls Pi, caches state in SQLite, forwards webhooks
Discordnoisebell-discord3001Posts door status embeds to Discord
RSSnoisebell-rss3002Serves Atom feed of recent door events

Cache API

MethodPathAuthDescription
GET/statusNoneCurrent door status (status, timestamp, last_seen)
GET/infoNonePi system info (JSON blob)
GET/historyNoneLast 100 state changes
POST/webhookBearer tokenInbound webhook from Pi
GET/healthNoneReturns 200 OK

Building

All remote services live in a Cargo workspace under remote/.

sh
1cd remote
2cargo build --release

Or with Nix:

sh
1cd remote
2nix build .#noisebell-cache
3nix build .#noisebell-discord
4nix build .#noisebell-rss

NixOS Configuration

The flake at remote/flake.nix exports NixOS modules. Example configuration:

nix
1{
2 inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote";
3
4 outputs = { self, nixpkgs, noisebell, ... }: {
5 nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
6 system = "x86_64-linux";
7 modules = [
8 noisebell.nixosModules.default
9 ({ ... }: {
10 services.noisebell-cache = {
11 enable = true;
12 domain = "cache.noisebell.example.com";
13 piAddress = "http://noisebell-pi:80";
14 piApiKeyFile = "/run/secrets/noisebell-pi-api-key";
15 inboundApiKeyFile = "/run/secrets/noisebell-inbound-api-key";
16 outboundWebhooks = [
17 {
18 url = "http://localhost:3001/webhook";
19 secretFile = "/run/secrets/noisebell-discord-webhook-secret";
20 }
21 ];
22 };
23
24 services.noisebell-discord = {
25 enable = true;
26 domain = "discord.noisebell.example.com";
27 discordTokenFile = "/run/secrets/noisebell-discord-token";
28 channelId = "123456789012345678";
29 webhookSecretFile = "/run/secrets/noisebell-discord-webhook-secret";
30 };
31
32 services.noisebell-rss = {
33 enable = true;
34 domain = "rss.noisebell.example.com";
35 cacheUrl = "http://localhost:3000";
36 };
37 })
38 ];
39 };
40 };
41}

Secrets

SecretUsed byDescription
piApiKeyFileCacheBearer token for authenticating to Pi GET endpoints
inboundApiKeyFileCacheBearer token the Pi uses when POSTing to /webhook
outboundWebhooks[].secretFileCacheBearer token sent with outbound webhook POSTs
discordTokenFileDiscordDiscord bot token
webhookSecretFileDiscordMust match the cache's outbound webhook secret