๐ A smart DMD clock with a plugin system, for ZeDMD, Pixelcade, and PIN2DMD displays.
Tired of simple DMD clocks that only show time and maybe a GIF? zeClock is a full-featured DMD clock with built-in plugins (pinball animations, weather, stocks, pong, speaker timer and more), a plugin system to create your own, and connectivity to your ecosystem via REST API, MQTT, and Home Assistant.
zeClock can drive ZeDMD hardware directly via libzedmd, or output to any device supported by libdmdutil's dmdserver -- including ZeDMD, Pixelcade, and PIN2DMD -- with multiple devices active simultaneously.
It comes with a web interface for easy configuration, and a browser-based virtual renderer so you can try everything before buying ZeDMD-compatible hardware.
For an autonomous setup, install zeClock on a Raspberry Pi - ZeDMD is auto-detected via USB or WiFi. Just power the Pi and get going with a 24/7 clock.
Full length video: ๐บ www.youtube.com/watch?v=YdpWlg71KtM
You can configure zeClock through configuration files or a dedicated web UI.
And also configure how often a given plugin will appear, and even de-activate it.
No hardware required โ try zeClock in your browser in seconds:
git clone https://github.com/DMDTools/zeClock.git
cd zeClock
pip install -e ".[dev]"
zeclock --bootstrap
make dev-start-virtualOpen http://localhost:3000 โ the clock is running with a WebGL DMD shader in your browser!
For HD mode (256x64): make dev-start-virtual-hd ยท To stop: make dev-stop or Ctrl+C
zeClock alternates between a clock display and animated plugins. Each plugin renders content on the DMD for a few seconds, then returns to the clock.
Retro pinball DMD animations with clock overlay. Plays randomly from 2300+ .scn animation files with DotBlt composition. Powered by DotClk-Resources, kindly shared by SigmaFX with authorization to use the proprietary .scn animations.
Two AI players compete in a real Pong match. The score persists across activations โ the clock only takes over between points. First to 5 wins with confetti celebration.
Current conditions, tomorrow's forecast, 3-day outlook, and 7-day overview. Data from Open-Meteo API (no key required), cached 15 minutes.
Animated robot eyes tracking a buzzing fly. The eyes react with expressions (surprised, annoyed, sleepy) based on the fly's behavior.
Real-time stock prices from Yahoo Finance with intraday sparkline graphs. Supports multiple symbols, pre/post market data.
Plays animated GIFs with support for multiple source directories and weighted random rotation. Drop .gif files in ~/.zeclock/plugins/gif/ or configure multiple directories.
A great source: 11,000+ DMD GIF pack (donationware, with a free 600 GIF pack available at the same link).
Conference countdown timer visible from stage. Control via Web UI or REST API: set duration, start, pause, reset. The progress bar transitions from green โ orange (20% remaining) โ red (10% remaining), then blinks at 00:00 when elapsed.
You can also send messages to the display at any time through the Web UI or API (e.g. "WRAP UP!", "5 min remaining!").
AI-based paragliding flyability forecast from Paraglidable. Cycles through configured spots showing today's FLY/XC/Takeoff percentages (color-coded: green โฅ80%, orange โฅ60%, red <60%), plus a 3-day forecast page per spot. Data cached 30 minutes. Requires a free API key.
Real-time intrusion detection alerts from Reolink cameras. Connects via the Baichuan TCP push protocol for instant event delivery โ no polling. Displays a localized blinking alert when a person, vehicle, animal, or motion is detected:
- ๐ด Person โ red blinking border
- ๐ Vehicle โ orange blinking border
- ๐ข Animal โ green blinking border
- ๐ก Motion โ yellow blinking border
Messages are localized (en/fr/de/es) via the global language setting. The alert overrides whatever is currently displayed and disappears after a configurable duration.
Plugins are configured in ~/.zeclock/config/plugins.yaml:
language: fr
clock_display_seconds: 10
plugins:
- name: pinball
frequency: 30
- name: pong
frequency: 20
- name: weather
frequency: 20
settings:
city_name: Grenoble
latitude: 45.19
longitude: 5.72
- name: eyes
frequency: 10
- name: stock
frequency: 10
settings:
symbols: AAPL,MSFT,^FCHI
page_duration_seconds: 5
- name: gif
frequency: 15
settings:
gif_dirs:
- path: "~/.zeclock/plugins/gif/Arcade"
weight: 30
recursive: true
- path: "~/.zeclock/plugins/gif/Movies"
weight: 10
recursive: false
- name: speaker-timer
frequency: 0
settings:
yellow_threshold: 20
red_threshold: 10
- name: paragliding
frequency: 15
settings:
api_key: YOUR_PARAGLIDABLE_API_KEY
# Optional: filter to specific spots (comma-separated substrings)
# spots: "St Hilaire,Chamrousse"
page_duration_seconds: 5- language: Global language for the UI and all plugins (en, fr, de, es)
- clock_display_seconds: How long the clock is shown between plugins (default: 5)
- frequency: Relative weight for plugin selection (higher = more often). Set to 0 to disable a plugin without removing it.
Override from CLI: zeclock --plugins pinball,pong
zeClock's plugin system is designed for extensibility. Create a single Python file, drop it in ~/.zeclock/plugins/, and it's automatically discovered at startup.
A minimal plugin is ~30 lines of code:
from PIL import Image
from zeclock.plugins.base import ClockPlugin
class MyPlugin(ClockPlugin):
@property
def name(self) -> str:
return "my-plugin"
@property
def description(self) -> str:
return "My custom plugin"
@property
def frame_delay_ms(self) -> int:
return 100 # 10 FPS
async def initialize(self, config: dict) -> None:
pass
async def render_frame(self, width: int, height: int):
img = Image.new("RGB", (width, height), (0, 0, 0))
# Draw your content on img using Pillow
return img
async def cleanup(self) -> None:
passThe full guide covers multi-page plugins, config schemas (auto-generated web UI forms), confetti animations, upscaling helpers, error handling, and testing:
| Plugin | Setting | Description |
|---|---|---|
| weather | city_name |
City name (triggers geocoding if no lat/lon) |
latitude / longitude |
Coordinates for weather data | |
temperature_unit |
celsius (default) or fahrenheit |
|
page_duration_seconds |
Duration per weather page (default: 4) | |
| stock | symbols |
Comma-separated tickers (e.g. AAPL,MSFT,^FCHI) |
page_duration_seconds |
Duration per stock page (default: 5) | |
| gif | gif_dirs |
List of directory entries (see example above) |
Each entry: path |
Directory containing .gif files (required) |
|
weight |
Selection probability (default: 50) | |
recursive |
Search subdirectories (default: true) | |
| speaker-timer | yellow_threshold |
% remaining to switch to orange (default: 20) |
red_threshold |
% remaining to switch to red (default: 10) | |
| pinball | color |
Animation color name (default: orange) |
animation_color |
Separate animation color (default: complementary of color) |
|
| pong | color |
Game color name (default: orange) |
| eyes | color |
Eyes color name (default: cyan) |
| paragliding | api_key |
Paraglidable API key (required, free from paraglidable.com) |
spots |
Comma-separated spot name filters (optional) | |
page_duration_seconds |
Duration per page (default: 5) | |
| reolink-alert | camera_host |
IP address or hostname of the Reolink camera (required) |
camera_user |
Camera login username (default: admin) |
|
camera_password |
Camera login password (required) | |
camera_channel |
Camera channel, 0 for single cam (default: 0) | |
alert_duration |
Display duration in seconds (default: 15) | |
cooldown_seconds |
Minimum time between alerts (default: 10) |
# With pipx:
pipx install git+https://github.com/DMDTools/zeClock.git
# OR with uvx (no install needed):
uvx --from git+https://github.com/DMDTools/zeClock.git zeclockgit clone https://github.com/DMDTools/zeClock.git
cd zeClock
pip install -e ".[dev]"On first launch, zeClock downloads resources automatically. Or run manually:
zeclock --bootstrapThis installs to ~/.zeclock/:
- libzedmd โ native ZeDMD communication library
- 2300+ retro animations (
.scn) and bitmap fonts (.fnt) from DotClk-Resources
# Auto-detect backend and connection
zeclock
# Fixed orange clock
zeclock --color orange
# Only specific plugins
zeclock --plugins pinball,weather| Option | Values | Default | Description |
|---|---|---|---|
--backend |
auto, zedmd, dmdserver |
auto |
Backend selection (details) |
--wifi-addr |
IP address | from config | ZeDMD WiFi address |
--device |
path | auto-detect | USB serial device |
--brightness |
0-15 | 10 | Display brightness |
--hd |
โ | โ | Force HD resolution (256x64) |
--color |
color name | auto |
Clock color (auto = rotate every minute) |
--plugins |
comma-separated | all | Only activate listed plugins |
--bootstrap |
โ | โ | Install resources |
~/.zeclock/config/zeclock.ini:
[zedmd]
wifi_addr = 192.168.0.35
brightness = 10
[display]
font = STANDARD
[location]
# Used by brightness scheduling (sunrise/sunset auto-brightness)
# Note: the weather plugin has its own location in plugins.yaml
latitude = 45.1885
longitude = 5.7245
city_name = Grenoble
[brightness_schedule]
max_brightness = 7
default = 22:00-08:00 10%
sunrise_brightness = 100%
sunset_brightness = 10%
# time_only = 22:00-08:00
[rest_api]
enabled = true
host = 0.0.0.0
port = 8080
[dmdserver]
host = localhost
port = 6789Brightness is percentage-based (0โ100%). 100% maps to max_brightness hardware level.
[brightness_schedule]
# Per-day (overnight ranges supported)
default = 22:00-08:00 10%
monday = 08:00-18:00 80%, 22:00-08:00 10%
# Or sunrise/sunset auto (requires [location])
sunrise_brightness = 100%
sunset_brightness = 10%
# "Time only" mode: clock only, no plugins during these hours
time_only = 22:00-08:00Enable in config, then control via HTTP:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/status |
Current status |
POST |
/api/screen/on |
Turn screen on |
POST |
/api/screen/off |
Turn screen off |
POST |
/api/plugin/force |
Force plugin {"plugin": "name"} |
POST |
/api/plugin/resume |
Resume normal rotation |
POST |
/api/text |
Display text {"text": "...", "duration": 10} |
POST |
/api/alert |
Rich alert with blinking border {"text": "...", "duration": 15, "icon": "person", "color": [255,0,0]} |
POST |
/api/speaker-timer/set |
Set timer {"seconds": 300} |
POST |
/api/speaker-timer/start |
Start/resume timer |
POST |
/api/speaker-timer/pause |
Pause timer |
POST |
/api/speaker-timer/reset |
Reset timer |
[mqtt]
enabled = true
host = 192.168.1.100
port = 1883
device_id = zeclock
ha_discovery = trueInstall: pip install zeclock[mqtt]
Topics: zeclock/<device_id>/command, zeclock/<device_id>/state, zeclock/<device_id>/availability
When ha_discovery = true, zeClock auto-registers in Home Assistant (switch + sensors).
ZeDMD hardware optional โ browser emulation at http://localhost:3000.
Plug in a Raspberry Pi, power on โ zeClock auto-detects ZeDMD (USB or WiFi) and runs 24/7 unattended.
- Raspberry Pi 4 (or 5) with WiFi
- microSD card (8 GB+)
- ZeDMD display (USB or WiFi โ auto-detected)
- Power supply for the Pi and ZeDMD
Download the latest zeclock-rpi.img.xz from GitHub Releases.
Flash it with Raspberry Pi Imager, balenaEtcher, or from the command line:
xzcat zeclock-rpi.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
sync- Insert the SD card into the Pi
- Plug ZeDMD into a USB port on the Pi
- Power on the Pi
On first boot, the Pi doesn't know your WiFi yet. After ~30 seconds it creates its own hotspot:
- On your phone or laptop, connect to the WiFi network zeClock-Setup
- A captive portal opens automatically โ select your home WiFi and enter the password
- The Pi connects, and zeClock starts within seconds
That's it! WiFi credentials are saved โ next time you power on, it connects automatically.
The clock is running. The display survives power cuts โ just unplug/replug anytime.
The web UI is available at http://zeclock.local:8080 (REST API enabled by default).
You can also SSH in:
ssh zeclock@zeclock.local
# Default password: zeclockConfiguration lives in /data/zeclock/config/:
nano /data/zeclock/config/zeclock.ini # display, brightness, backend
nano /data/zeclock/config/plugins.yaml # plugins and their settings
sudo systemctl restart zeclock # apply changesThe image is built automatically via GitHub Actions on a native ARM64 runner (~2 min). See the Releases page.
To add your GIF collection to the image locally (WSL2 or Linux):
make rpi-inject-gifs # Downloads CI image + injects GIFs from ~/.zeclock/plugins/gifSee deploy/rpi/ for the full Packer build system and architecture details.
make test # Run tests + lint + type check
make dev-start-virtual # Virtual DMD in browser (128x32)
make dev-start-virtual-hd # Virtual DMD HD (256x64)See CONTRIBUTING.md for full details.
libzedmd not found โ Run zeclock --bootstrap to install.
ZeDMD not detected (WiFi) โ Check ping <ip> and set wifi_addr in config or via --wifi-addr.
ZeDMD not detected (USB) โ List ports with ls /dev/ttyUSB* and use --device.
Animations not displaying โ Run zeclock --bootstrap to reinstall resources.
- sigmafx/DotClk โ Original DotClk inspiration
- sigmafx/DotClk-Resources โ 2300+ animations
- PPUC/libzedmd โ ZeDMD native library
- PPUC/ZeDMD โ ZeDMD hardware
- vpinball/libdmdutil โ DMD utility library with
dmdserverfor multi-device output (ZeDMD, Pixelcade, PIN2DMD)
MIT โ see LICENSE
Made with โค๏ธ by ojacques โ Inspired by the magic of retro pinball ๐ฎโจ












