Communication between unify and teams for doorbell messages.
  • JavaScript 96.5%
  • Shell 3.5%
Find a file
SPooKfpv bee7c69741 fix: use fresh ring-event thumbnail for snapshots
The camera /snapshot endpoint returns a stale cached frame on this doorbell
(verified: identical bytes across params/time, even though recording is active),
so every Teams card showed the same image. Fetch the newest ring/person-detection
event thumbnail instead (what the Protect app shows), falling back to the camera
snapshot only when no recent event thumbnail exists.
2026-06-11 10:30:57 +02:00
docs/plans feat: weekly cleanup, 7-day snapshots, Protect reconnect, Pi 5 deploy 2026-06-10 14:41:00 +02:00
scripts chore: mark scripts/install.sh executable 2026-06-10 15:12:34 +02:00
src fix: use fresh ring-event thumbnail for snapshots 2026-06-11 10:30:57 +02:00
systemd feat: weekly cleanup, 7-day snapshots, Protect reconnect, Pi 5 deploy 2026-06-10 14:41:00 +02:00
tests fix: use fresh ring-event thumbnail for snapshots 2026-06-11 10:30:57 +02:00
.env.example feat: weekly cleanup, 7-day snapshots, Protect reconnect, Pi 5 deploy 2026-06-10 14:41:00 +02:00
.gitignore feat: complete doorbell-to-Teams notification service 2026-03-08 17:25:28 +01:00
.nvmrc init: project setup with ESM package.json and structure 2026-03-08 15:27:14 +01:00
package-lock.json feat: migrate snapshot storage from Azure Blob to S3 with self-hosted endpoint support 2026-03-10 08:57:30 +01:00
package.json feat: migrate snapshot storage from Azure Blob to S3 with self-hosted endpoint support 2026-03-10 08:57:30 +01:00
README.md fix: use fresh ring-event thumbnail for snapshots 2026-06-11 10:30:57 +02:00
vitest.config.js feat: complete doorbell-to-Teams notification service 2026-03-08 17:25:28 +01:00

Doorbell → Teams

Sends a Microsoft Teams notification (with a snapshot image) when your UniFi Protect doorbell is pressed. Supports multiple doorbells with custom messages per doorbell.

Requirements

  • Node.js 22+ (nvm use to apply .nvmrc)
  • A UniFi Protect controller (NVR or UDM) with one or more doorbell cameras
  • A Microsoft Teams Workflows webhook URL (see setup below)
  • S3-compatible storage: an AWS S3 account or a self-hosted service (RustFS, MinIO) (see setup below)

Quick Start

1. Clone and install

git clone <repo-url>
cd doorbell-teams
npm install

2. Create your .env file

cp .env.example .env

Edit .env with your values (see Configuration below).

3. Get your camera IDs

Find the camera ID(s) from your UniFi Protect controller. The easiest way is to log into Protect, go to your doorbell camera settings, and copy the ID from the URL. Alternatively, you can temporarily add a console.log(ufp.bootstrap.cameras) log to src/protect.js after bootstrap to print all camera IDs.

4. Create a Teams Workflows webhook

Note: The old Office 365 Connector "Incoming Webhook" is being retired on April 30, 2026. Use Power Automate Workflows instead.

  1. In Teams, go to the channel where you want notifications
  2. Click +Workflows → search for "Post to a channel when a webhook request is received"
  3. Follow the wizard and copy the webhook URL into TEAMS_WEBHOOK_URL

5. Set up S3-compatible storage

Snapshots are uploaded to an S3-compatible bucket and delivered to Teams via pre-signed URLs valid for 7 days, so the inline image and the click-to-open-full-res link keep working for the whole week. Using a URL (instead of a base64-embedded image) also sidesteps the 28KB Adaptive Card payload limit.

This works with AWS S3 or a self-hosted S3-compatible service (RustFS, MinIO, etc.):

  • AWS S3: create an IAM user with S3 access (e.g. the AmazonS3FullAccess policy or a bucket-scoped one), generate an access key, and put the Access key ID and Secret access key into .env. Optionally set AWS_REGION (default us-east-1).
  • Self-hosted (RustFS / MinIO): set S3_ENDPOINT to your endpoint URL (e.g. https://rustfs.example.com) plus the access key and secret. Path-style addressing is enabled automatically when S3_ENDPOINT is set.

The bucket (default doorbell-snapshots, override with AWS_S3_BUCKET) is auto-created on startup.

Security & cleanup: objects use random UUID names, and pre-signed URLs are read-only. Every object in the bucket is deleted at the weekly cleanup (Sunday 03:00 local time) — see Cleanup & retention.

6. Configure your doorbells

In .env, set the DOORBELLS variable with your camera IDs, display names, and custom messages:

DOORBELLS=cam-abc123|Front Door|Someone at the front door;cam-def456|Delivery|Package delivery at back entrance

Format: cameraId|Display Name|Custom Message — separate multiple doorbells with semicolons (;).

Note: Custom messages can contain commas, but not semicolons.

7. Start the service

npm start

You should see:

Service ready. Monitoring 2 doorbells: Front Door, Delivery

Press your doorbell — a Teams card with a snapshot should appear in your channel.

Configuration

All configuration is via environment variables. Copy .env.example to .env and fill in the values.

Variable Required Default Description
PROTECT_HOST Yes - IP or hostname of your UniFi Protect NVR/UDM
PROTECT_USERNAME Yes - Local UniFi account username
PROTECT_PASSWORD Yes - Local UniFi account password
DOORBELLS Yes* - Multi-doorbell config: camId|Name|Message;camId2|Name2|Message2
PROTECT_CAMERA_ID Yes* - (Deprecated) Single doorbell fallback. Use DOORBELLS instead.
TEAMS_WEBHOOK_URL Yes - Teams Workflows webhook URL
AWS_ACCESS_KEY_ID Yes - S3 access key ID
AWS_SECRET_ACCESS_KEY Yes - S3 secret access key
AWS_REGION No us-east-1 AWS region (ignored by most self-hosted S3 services)
S3_ENDPOINT No - S3-compatible endpoint URL (RustFS/MinIO). Omit for AWS S3; enables path-style addressing
AWS_S3_BUCKET No doorbell-snapshots Bucket name (auto-created on startup)
DEBOUNCE_MS No 10000 Minimum milliseconds between ring notifications (per doorbell)
PROTECT_HEALTHCHECK_MS No 300000 Interval (ms) between Protect liveness probes; forces a re-login after repeated failures (0 disables)

* Either DOORBELLS or PROTECT_CAMERA_ID is required. DOORBELLS is recommended.

Cleanup & retention

Snapshots and notification cards are kept for the week, then wiped once a week at Sunday 03:00 local time:

  • Snapshots: every object in the S3 bucket is deleted. (Pre-signed URLs are valid 7 days, so images and click-to-open links work until the sweep.)
  • Teams cards (optional): if the five TEAMS_* Graph API variables are set, the service also soft-deletes its own doorbell cards at the weekly sweep, so old cards don't linger with broken image links. Cards are identified by a hidden marker, so on a shared channel other people's messages are never touched. Without the Graph variables, snapshots are still cleaned up but cards remain.

Graph API setup (optional) is documented in .env.example. Register an Entra ID (Azure AD) app, grant it ChannelMessage.ReadWrite.All application permission with admin consent, and set all five of TEAMS_TENANT_ID, TEAMS_CLIENT_ID, TEAMS_CLIENT_SECRET, TEAMS_TEAM_ID, TEAMS_CHANNEL_ID (all or none).

Note: the marker only tags cards posted after this version. Any doorbell cards posted earlier won't be auto-deleted — clear them once by hand.

Multi-Doorbell Configuration

The DOORBELLS environment variable allows you to monitor multiple doorbell cameras, each with its own custom notification message.

Format:

DOORBELLS=cameraId|Display Name|Custom Message;cameraId2|Display Name 2|Custom Message 2
  • Semicolons (;) separate doorbells
  • Pipes (|) separate the three fields within each doorbell entry
  • Each doorbell has three fields:
    1. Camera ID — the UniFi Protect camera ID
    2. Display Name — shown in logs (e.g., "Front Door")
    3. Custom Message — shown in the Teams card (e.g., "Someone at the front door")

Example:

DOORBELLS=cam-abc|Front Door|Someone at the front door;cam-def|Delivery|Delivery entrance

Limitations:

  • Custom messages cannot contain semicolons (;) — semicolons are used as delimiters between doorbells
  • All three fields are required and must be non-empty

Backward Compatibility

If you have an existing setup using PROTECT_CAMERA_ID, it will continue to work. The service will automatically convert it to a single doorbell with the default message "Someone is at the door!". To customize the message, migrate to the DOORBELLS format.

Architecture

UniFi Protect (WebSocket)
        │
        ▼
src/protect.js  ─── ring event (per camera) ───▶  src/handler.js (per doorbell)
        │                                                  │
        │                                                  ▼
        │                                         fetch snapshot buffer
        │                                                  │
        │                                                  ▼
        │                                         src/storage.js
        │                                         (upload to S3-compatible store)
        │                                                  │
        │                                                  ▼
        │                                         generate pre-signed URL (7-day expiry)
        │                                                  │
        │                                                  ▼
        │                                          src/teams.js
        └─────────────────────────────────────▶  (Adaptive Card with image URL + hidden marker)

   Weekly (Sunday 03:00 local): wipe every snapshot in the bucket
   and soft-delete this app's own marked Teams cards.
Module Purpose
src/config.js Loads and validates environment variables, parses multi-doorbell config
src/protect.js Wraps unifi-protect: login, WebSocket events, fresh per-ring snapshots (event thumbnail, falls back to the camera snapshot), reconnect re-bootstrap + health-check watchdog
src/storage.js S3-compatible client: uploads snapshots, generates 7-day pre-signed URLs, deletes the whole bucket at cleanup
src/teams.js Posts an Adaptive Card (with a hidden cleanup marker); soft-deletes its own marked cards via Graph at cleanup
src/handler.js Per-doorbell debounce -> snapshot -> upload -> notify Teams; schedules the weekly Sunday-03:00 cleanup
src/index.js Entry point: one handler per doorbell; graceful shutdown stops the timer and disconnects (no wipe on restart)

How snapshots work: On a ring, the service grabs the newest ring/person-detection event thumbnail for that camera (the same fresh image the Protect app shows) — because the plain camera snapshot endpoint can return a stale, cached frame on some doorbells. If no recent event thumbnail is available, it falls back to the camera snapshot. The image (up to 1920x1080) is uploaded to the S3-compatible bucket with a random UUID filename. The service generates a read-only pre-signed URL valid for 7 days and includes it in the Teams Adaptive Card, so the image and click-to-open-full-res link work all week. Each card also carries a hidden marker used only by the weekly cleanup. Snapshots (and, if the Graph API is configured, this app's own cards) are removed at the weekly Sunday-03:00 sweep. If upload fails, the card falls back to text-only with "⚠️ Snapshot unavailable".

Running as a Service

Windows (NSSM)

nssm install DoorbellTeams "C:\Program Files\nodejs\node.exe" "C:\path\to\doorbell-teams\src\index.js"
nssm set DoorbellTeams AppDirectory "C:\path\to\doorbell-teams"
nssm set DoorbellTeams AppEnvironmentExtra "NODE_ENV=production"
nssm start DoorbellTeams

Raspberry Pi 5 (alongside WS_Display)

The repo ships a user-mode systemd unit (systemd/doorbell-teams.service) and an idempotent installer (scripts/install.sh) that mirrors the WS_Display deployment pattern. It runs as a user service, so it coexists with WS_Display with no port conflict (WS_Display binds 127.0.0.1:8080; this service has no inbound listener).

Prerequisites: Node 22+ on the Pi (nvm install 22 or NodeSource) and a filled-in .env.

cd /path/to/Unify-Teams
cp .env.example .env && $EDITOR .env     # fill in your values
scripts/install.sh

install.sh checks Node >= 22, runs npm ci --omit=dev, resolves the real node path into the unit, writes it to ~/.config/systemd/user/doorbell-teams.service, and enables + starts the service.

# Run ONCE so the service starts at boot (not just at login):
sudo loginctl enable-linger "$USER"

# Manage / inspect:
systemctl --user status doorbell-teams
journalctl --user -u doorbell-teams -f

Exit-code behavior: a transient failure (controller unreachable at boot) exits 1 and systemd restarts it. A permanent config error (bad .env) exits 2; RestartPreventExitStatus=2 stops the restart loop so it doesn't crash-loop. After fixing .env:

systemctl --user reset-failed doorbell-teams && systemctl --user restart doorbell-teams

Development

npm test               # Run all tests
npm run test:coverage  # Run tests with coverage report (≥ 80% enforced)
npm run dev            # Start with --watch for auto-reload

Troubleshooting

"Missing required environment variables" — check your .env file and make sure all required vars are set. You need either DOORBELLS or PROTECT_CAMERA_ID.

"DOORBELLS or PROTECT_CAMERA_ID required" — you must set at least one. Use DOORBELLS for multi-doorbell support or PROTECT_CAMERA_ID for single doorbell (legacy).

"Invalid DOORBELLS entry" — check the format: cameraId|Name|Message;cameraId2|Name2|Message2. All three fields are required for each doorbell, separated by pipes (|). Multiple doorbells are separated by semicolons (;).

"Cameras not found in UniFi Protect: cam1, cam2" — one or more camera IDs in your DOORBELLS config don't exist in your UniFi Protect system. The error message lists which cameras were found — check for typos in your camera IDs.

Teams card shows no image — check logs for "Snapshot failed" or "Failed to upload snapshot" messages. If the camera is offline or S3 upload fails, you'll see a card with "⚠️ Snapshot unavailable". Verify your AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and (for self-hosted) S3_ENDPOINT are correct and the bucket is reachable.

Ring detected but no Teams message — check that TEAMS_WEBHOOK_URL is correct and the Workflow is active. The service logs the Teams response status.

Notifications firing too often — increase DEBOUNCE_MS (default 10 seconds per doorbell). Each doorbell has independent debounce tracking.

Multiple doorbells, but only one sends notifications — check that all camera IDs in DOORBELLS are correct and that the service logs "Service ready. Monitoring N doorbells: ..." with all expected doorbell names at startup.