Skip to content

B2elks/ringklocka

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ringklocka — UniFi Protect doorbell receiver

WiFi-connected MP3 doorbell that plays a configurable or random chime when a UniFi Protect G4 Doorbell (or any HTTP webhook source) triggers it. Runs MicroPython on an ESP32-C3 SuperMini driving an OPEN-SMART Serial Control USB MP3 module.

  UniFi Protect / Home Assistant
              │ HTTP webhook
              ▼
   ┌────────────────────┐         ┌───────────────────────┐
   │  ESP32-C3 SuperMini│ UART    │ OPEN-SMART Serial MP3 │
   │  (MicroPython)     │ ──────▶ │  + microSD + speaker  │
   └────────────────────┘         └───────────────────────┘

Hardware

  • MCU: ESP32-C3 SuperMini (~50 SEK). WiFi 4 + native USB-Serial/JTAG.
  • Audio module: OPEN-SMART Serial Control USB MP3 v1.1. 3 W onboard amp, microSD up to 32 GB, headphone jack.
  • Speaker: 8 Ω 1–3 W on the module's XH2.54 connector.
  • Storage: Any microSD card ≤ 32 GB, FAT16 or FAT32.

Wiring (4 wires)

ESP32-C3 mini Open-Smart MP3
5V VCC
GND GND
GPIO 21 (UART1 TX) RX
GPIO 20 (UART1 RX) TX

GPIO numbers are configurable in main.py (UART_TX / UART_RX). Speaker connects to the XH2.54 header or the 3.5 mm jack on the module.

Optional 3D-printed case

See case/ for an OpenSCAD model and pre-built STL files (box + front panel + hex grille) sized for the speaker, the ESP32-C3, and the MP3 module. Build with make all (requires OpenSCAD in PATH).

SD card preparation

Format FAT16 or FAT32 (≤ 32 GB). Copy files named 001*.mp3, 002*.mp3, … into the root one at a time, in the order you want them indexed — the module reads files in FAT directory order, not alphabetically.

The chip is picky about MP3 encoding. Reliable settings: 22.05 kHz mono, 64 kbps CBR, no ID3 tags. Convert any source file with ffmpeg:

ffmpeg -i source.mp3 -ar 22050 -ac 1 -b:a 64k -id3v2_version 0 -map_metadata -1 001.mp3

Files at 48 kHz stereo VBR with ID3v2 tags are sometimes accepted but silently no-op often enough to be unreliable.

Update TRACK_COUNT in main.py to match the number of files.

Flashing

# 1. MicroPython 1.22+ firmware
esptool.py --chip esp32c3 --port /dev/tty.usbmodem* erase_flash
esptool.py --chip esp32c3 --port /dev/tty.usbmodem* --baud 460800 \
    write_flash 0x0 ESP32_GENERIC_C3-*.bin

# 2. Project files (edit WIFI_SSID/WIFI_PASS/TRACK_COUNT in main.py first)
mpremote connect /dev/tty.usbmodem* cp boot.py main.py mp3.py http_parse.py :

# 3. Reset and watch serial
mpremote connect /dev/tty.usbmodem* reset
mpremote connect /dev/tty.usbmodem*    # opens REPL/serial monitor

You should see within 30 s:

wifi: connected, ('192.168.x.y', '255.255.255.0', '192.168.x.1', '192.168.x.1')
http: listening on :80

Note the IP and add a DHCP reservation in your router so it stays fixed.

HTTP API

# Play track N at default volume (25/30)
curl "http://<ip>/ring?track=1"

# Play random track at custom volume (0..30)
curl "http://<ip>/ring?track=random&vol=20"

# POST equivalent
curl -X POST -H 'Content-Type: application/json' \
     -d '{"track":"random","vol":15}' \
     http://<ip>/ring

# Health check
curl http://<ip>/health
Endpoint Method Response
/ring?track=N&vol=V GET 200 {"status":"playing","track":N} or 409 {"status":"busy"}
/ring?track=random&vol=V GET 200 with the randomly chosen track
/ring POST (JSON) Same semantics as GET
/health GET 200 {"ok":true,"playing":bool}

Validation errors return 400 {"error":"…"}. Method not allowed → 405. No auth — relies on LAN isolation; add a reverse proxy if you need it.

UniFi Protect integration

UniFi Protect doesn't send HTTP webhooks natively as of firmware 4.x. Two integration paths:

Via Home Assistant (recommended)

  1. Install the UniFi Protect integration in Home Assistant.

  2. Add an automation triggered on the "doorbell" sensor event:

    alias: Ring the chime
    triggers:
      - platform: state
        entity_id: binary_sensor.frontdoor_doorbell
        to: "on"
    actions:
      - action: rest_command.ring_doorbell
  3. Define the rest_command:

    rest_command:
      ring_doorbell:
        url: "http://192.168.1.95/ring?track=random&vol=25"
        method: get

Via UniFi OS Webhook (alternative, requires UDM/UNVR)

UniFi has a beta webhook action in Network Application. Configure it to GET http://<ringklocka-ip>/ring?track=random&vol=25 and bind to the doorbell device event. Behaviour varies between firmware versions.

Hardware integration notes

Found during real flashing + testing on a 2026-05 batch of the OPEN-SMART module:

  1. The module boots in U-disk mode. A [Set IC mode] reset (7E 03 35 05 EF) followed by [Select device] TF card (7E 03 35 01 EF) is required on every power-up. We do this in setup_player() with 2 s and 1 s settle times — shorter intervals are unreliable.
  2. Per-play select_sd. The module silently reverts to U-disk between plays on this firmware revision; _handle_ring() re-selects SD before every play_index() call.
  3. MicroPython incompatibility: bytes((cmd, *data)) is rejected by MicroPython (SyntaxError: *x must be assignment target). Use bytes((cmd,)) + bytes(data).
  4. Playback grace period: the module takes 500–800 ms to load the file and report PLAYING via get_status. Without an 800 ms grace before polling, the busy lock clears immediately.
  5. USB-Serial-JTAG quirk: after flashing, the port can stick. Unplug and replug if mpremote returns "could not enter raw repl".

Tests (host)

cd ringklocka
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest tests/ -v

Covers the pure modules (mp3.build_frame, Mp3Player against a fake UART, http_parse.*). The async server and hardware integration are verified manually with the checklist below.

Manual verification checklist

After flashing, run through each:

  1. Boot: serial shows wifi: connected + IP within 30 s.
  2. curl http://<ip>/ring?track=1 → 200, audible playback.
  3. curl "http://<ip>/ring?track=random&vol=10" → 200, random track, low volume.
  4. Two curl calls within ~100 ms → first 200, second 409 busy.
  5. After playback ends, curl http://<ip>/health{"playing":false}.
  6. curl http://<ip>/ring?track=99 → 400 track out of range.
  7. curl http://<ip>/ring?track=abc → 400 track must be an integer or 'random'.
  8. curl http://<ip>/ring → 400 missing track.
  9. WiFi resilience: power off router for 30 s, restore — within ~10 s the serial log shows reconnect; new requests succeed.
  10. curl http://<ip>/health → 200 {"ok":true,"playing":false}.

Repo layout

ringklocka/
├── boot.py          # runs main on power-up
├── main.py          # WiFi, async HTTP server, watch tasks
├── mp3.py           # Mp3Player UART driver + build_frame
├── http_parse.py    # request line + query parser
├── case/            # OpenSCAD 3D-printable enclosure
└── tests/           # pytest suite (host-runnable)

License

MIT.

About

UniFi Protect doorbell receiver: ESP32-C3 + OPEN-SMART MP3 module + MicroPython webhook server

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors