Remote bells and webhooks

School Bell supports three different remote-control paths. Choose the smallest one that matches the installation:

Feature

Purpose

Top-level trigger

Copy every scheduled signal to another Linux bell over SSH.

manual_bell.remote_bells

Start best-effort SSH commands or webhooks when the physical button is pressed.

Top-level webhook

Accept authenticated requests that ring this node locally.

Remote connectivity is optional. A failed remote action is logged but does not make a correctly configured local bell dependent on a central service.

Prepare SSH access

SSH actions must work non-interactively as the School Bell service user. On the sending Pi, create a dedicated key and copy its public key to the receiving Pi:

$ ssh-keygen -t ed25519 -C school-bell -f /home/pi/.ssh/id_school_bell
$ ssh-copy-id -i /home/pi/.ssh/id_school_bell.pub pi@pibell-yard.local

Protect the private key and do not commit it. A service cannot answer a key passphrase prompt, so use a deliberately protected service key and restrict the receiving account and network access appropriately.

Create /home/pi/.ssh/config on the sender:

Host pibell-yard
    HostName pibell-yard.local
    User pi
    IdentityFile /home/pi/.ssh/id_school_bell
    IdentitiesOnly yes
    PreferredAuthentications publickey

Connect once and verify the host identity before unattended operation:

$ ssh pibell-yard /usr/bin/aplay --help

Run this test as the same account used by school-bell.service. It must complete without asking for a password, passphrase or host confirmation.

Warning

The current built-in SSH command disables strict host-key checking for compatibility with unattended installations. Register and verify the host yourself, restrict the dedicated key on the receiving Pi, and only use SSH triggering over a trusted management network. Host-key pinning should be added before treating an untrusted network as safe.

Mirror scheduled signals over SSH

The legacy top-level trigger mapping sends every scheduled signal to each listed host. Its value is the WAVE directory on that remote host:

{
  "trigger": {
    "pibell-yard": "/home/pi/samples"
  }
}

The remote directory must contain the same WAVE filenames selected by the local wav mapping, and /usr/bin/aplay must be available remotely. Test every destination before adding it to a production schedule.

Remote actions from a physical button

manual_bell.remote_bells can execute a specific SSH command or call an HTTP(S) webhook when the physical input is accepted:

{
  "manual_bell": {
    "gpio": 17,
    "wav_key": "lesson",
    "remote_bells": [
      {
        "transport": "ssh",
        "host": "pibell-yard.local",
        "user": "pi",
        "command": ["/usr/local/bin/manual-bell"],
        "timeout": 10
      },
      {
        "transport": "webhook",
        "url": "https://remote-bell.example.com/bell",
        "auth": {
          "type": "bearer",
          "token": "replace-with-remote-token"
        },
        "timeout": 5
      }
    ]
  }
}

These actions start in background threads and do not delay the local signal. Their success or failure is reported as manual_remote_trigger. A failed remote action does not undo or change the local result.

On a trusted local network, prefer a stable local IP address such as http://192.168.1.25:8081/bell when hostname or mDNS resolution adds noticeable latency. A hostname is more readable and easier to renumber, but it depends on reliable local name resolution. Using an IP address avoids that lookup; it does not compensate for a slow HTTP response from the receiving service.

Expose a bell webhook

Enable the authenticated endpoint on the receiving node:

{
  "webhook": {
    "enabled": true,
    "host": "127.0.0.1",
    "port": 8081,
    "token": "replace-with-a-long-random-token",
    "rate_limit": 10,
    "rate_window": 60
  }
}

The built-in server provides plain HTTP. Keep it on 127.0.0.1 and expose it through a TLS reverse proxy such as nginx, Caddy or Apache. Configure certificate validation, request-size limits and trusted-network access at the proxy. Never expose the built-in port directly to an untrusted network.

Local HTTPS with Caddy

Caddy can provide HTTPS without a public domain or internet-facing service. It creates a local certificate authority (CA), issues and renews the server certificate, and proxies requests to School Bell over loopback. Install Caddy using the official installation instructions, then create or extend /etc/caddy/Caddyfile on the receiving node:

pibell-yard.local {
    tls internal
    request_body {
        max_size 4KB
    }
    reverse_proxy 127.0.0.1:8081
}

Use a hostname that resolves to the receiving node from every sending node. Local DNS or mDNS can provide this; a controlled /etc/hosts entry is also sufficient. The hostname in the webhook URL must match the hostname in the Caddyfile.

Validate and load the configuration:

$ sudo caddy validate --config /etc/caddy/Caddyfile
$ sudo systemctl reload caddy

When Caddy runs as a systemd service, its local root certificate is normally stored at:

/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt

Install a copy of this public root certificate in the trust store of every sending node. Do not copy any private key from Caddy’s data directory. On a Debian or Raspberry Pi OS sender, copy the certificate securely to the node and run:

$ sudo install -m 0644 root.crt \
    /usr/local/share/ca-certificates/school-bell-caddy.crt
$ sudo update-ca-certificates

The bearer token remains required after enabling HTTPS. Test the complete path from a sending node, without disabling certificate verification:

$ curl -X POST https://pibell-yard.local/bell \
    -H 'Authorization: Bearer replace-with-a-long-random-token' \
    -H 'Content-Type: application/json' \
    --data '{"wav_key":"lesson"}'

Keep Caddy’s data directory persistent because it contains the local CA. Back up and protect it like other private key material. All proxied requests reach the built-in rate limiter from the loopback address, so its limit is shared by all clients using this proxy.

Send exactly one configured WAVE key:

$ curl -X POST https://bell.example.com/bell \
    -H 'Authorization: Bearer replace-with-a-long-random-token' \
    -H 'Content-Type: application/json' \
    --data '{"wav_key":"lesson"}'

HTTP status

Meaning

202

The local signal was accepted for background execution.

400

The JSON or wav_key is invalid.

401

Bearer authentication failed.

404

The request path is not /bell.

405

The endpoint only accepts POST.

409

Another signal is already active.

429

The per-client rate limit was exceeded.

503

Background execution could not be started.

Responses contain keys and status information, never sample paths. Webhook signals return 202 immediately after atomic acceptance; playback completion or failure is reported through the webhook_bell_* monitoring events. They are manual overrides: they do not respect holiday or disable-calendar suppression and they do not automatically invoke the legacy top-level SSH trigger destinations.

Verify and diagnose

  1. Run school-bell /home/pi/schema.json --check on every node.

  2. Test local audio and GPIO before adding a remote path.

  3. Test SSH as the systemd service user or test the webhook through its final TLS URL.

  4. Trigger one bell while another signal is active and confirm the expected 409 or skipped event.

  5. Inspect journalctl and the Graylog manual_remote_trigger, remote_trigger or webhook_* events.

See Configuration reference for every field and Networking and time synchronization for firewall and outage behavior.