This document is LLM-generated.
An always-on digital bedroom clock, as written about on Ars Technica: a Raspberry Pi Zero W (or Zero 2 W) driving an Adafruit 1.2" 4-digit 7-segment display (HT16K33) over I2C. It shows 24-hour local time, keeps itself accurate over NTP, and follows daylight-saving transitions automatically — so it never needs to be reset.
The Pi has no battery-backed clock, so correctness comes from two things:
- NTP (
systemd-timesyncd, on by default) keeps the system clock accurate over Wi-Fi. - The timezone is set once on the Pi (e.g.
sudo timedatectl set-timezone America/New_York); the app reads local time, so Python'szoneinfoapplies DST rules automatically. No offset math, ever.
Before the clock has synced after a cold boot, the display shows ---- rather than a
confidently wrong time.
- Raspberry Pi Zero W or Zero 2 W, Raspberry Pi OS Lite (Debian 13 "trixie"). Both boards are supported by the same code; the only difference is a zeroconf version pin used by the optional HomeKit feature, which pip selects automatically by CPU architecture (see requirements-homekit.txt). The Zero W is armv6 (32-bit OS only); the Zero 2 W is armv7/aarch64 (32- or 64-bit).
- Adafruit 1.2" 4-digit 7-segment display + I2C backpack (HT16K33), default address
0x70. - I2C must be enabled (
sudo raspi-config nonint do_i2c 0).
Wiring and assembly are covered step-by-step in HARDWARE_RUNBOOK.md; the 3D-printable housing (STLs + Fusion source) is in 3d_print/.
Pure Python. The HT16K33 is a simple register-based I2C chip, so it's driven directly with
smbus2 — no Blinka/Adafruit stack. Layout:
| Path | Role |
|---|---|
| piclock/clock.py | Time → display rendering (pure, unit-tested) |
| piclock/display.py | HT16K33 driver + segment/colon encoding |
| piclock/timesync.py | "Is the clock NTP-synced yet?" check |
| piclock/config.py | Config with PICLOCK_* env overrides |
| piclock/schedule.py | Day/night schedule logic (pure, unit-tested) |
| piclock/control.py | Shared on/off + brightness state (thread-safe) |
| piclock/ctl.py | Control socket for one-off CLI commands |
| piclock/homekit.py | Optional HomeKit Lightbulb accessory |
| piclock/app.py | Main loop |
| main.py | Entry point |
| systemd/piclock.service | systemd unit |
| scripts/install.sh | venv + deps + service installer (run on the Pi) |
| scripts/piclockctl | CLI client: piclockctl on|off|bright|dim|brightness N |
| deploy/ | optional CI push-to-deploy template (adapt before use) |
| 3d_print/ | printable case (STLs + Fusion source; separately licensed, see below) |
The installer creates a dedicated clocker service account, deploys the app to
/opt/piclock owned by that account, builds its venv, and installs + enables the systemd
service:
git clone https://github.com/code-a-saurus/PiClock.git ~/piclock && cd ~/piclock
sudo scripts/install.shThe installer takes three optional flags, combinable as needed (e.g.
sudo scripts/install.sh --homekit --schedule):
--homekit— installs the HomeKit deps into the venv and enables the Lightbulb accessory, so the display can be controlled from the Home app / Siri.--schedule— enables the in-app day/night schedule (blank the display by day, wake it in the evening, dim it at night; times and levels tunable — see Configuration).--reboot— enables a monthly midday reboot that returns the appliance to a known-good state.
Each flag fully toggles its feature: re-running the installer without a flag disables that feature, so pass every flag you want active on each run.
clocker is a no-login system account whose only privilege is membership in the i2c
group (to reach /dev/i2c-1). It has no sudo access and needs none — the app only reads
time-sync status and writes to the display.
To run in the foreground for development (as your own user):
python3 -m venv venv
venv/bin/pip install -r requirements.txt
venv/bin/python main.pyVerify the display is on the bus (once the hardware is attached):
sudo apt install -y i2c-tools && i2cdetect -y 1 # expect a device at 0x70Set via environment variables (typically in the systemd unit):
| Variable | Default | Meaning |
|---|---|---|
PICLOCK_24H |
1 |
24-hour (1) or 12-hour (0) |
PICLOCK_BRIGHTNESS |
3 |
Display brightness, 0–15 |
PICLOCK_SCHEDULE |
0 |
Enable the in-app day/night schedule (blank by day, wake in the evening, dim at night) |
PICLOCK_OFF_AT |
08:00 |
Schedule edge: blank the display (HH:MM, local time) |
PICLOCK_ON_AT |
17:00 |
Schedule edge: show the display at the wake brightness |
PICLOCK_DIM_AT |
20:00 |
Schedule edge: dim to the night brightness |
PICLOCK_WAKE_BRIGHTNESS |
15 |
Level the schedule's evening wake applies, 0–15 |
PICLOCK_NIGHT_BRIGHTNESS |
0 |
Level the schedule's night dim applies, 0–15 (0 is still lit) |
PICLOCK_BLINK |
0 |
Blink the colon at 1 Hz (default: steady, no movement at night) |
PICLOCK_I2C_BUS |
1 |
I2C bus number |
PICLOCK_I2C_ADDRESS |
0x70 |
HT16K33 address |
PICLOCK_TICK |
0.2 |
Loop period, seconds |
PICLOCK_DISPLAY |
ht16k33 |
Set to null to run with no hardware (dev/testing) |
PICLOCK_CTL_SOCKET |
(empty) | Path of the piclockctl control socket; empty disables it (the unit sets /run/piclock/ctl.sock) |
The shipped systemd unit leaves everything at these defaults except PICLOCK_BRIGHTNESS=0
(dimmest — it's a dark-bedroom appliance) and the control-socket path. Edit
systemd/piclock.service to taste and re-run install.sh.
sudo systemctl status piclock
sudo systemctl restart piclock
journalctl -u piclock -fOne-off display commands from the shell (they behave like Siri commands — the day/night schedule re-asserts itself at its next on/off edge):
piclockctl off # blank the display (the clock keeps running)
piclockctl on # show it again
piclockctl bright # full brightness (level 15)
piclockctl dim # dimmest lit level (level 0)
piclockctl brightness 7 # any level 0-15install.sh creates a clockctl group that gates the control socket. Users who need to
run clock commands should be added to it: sudo usermod -aG clockctl <user> (log out and
back in to pick it up).
The time/format logic runs and is tested off-Pi (no hardware needed):
python3 -m venv venv
venv/bin/pip install -r requirements-dev.txt
venv/bin/pytest
# Exercise the full loop without a display:
PICLOCK_DISPLAY=null venv/bin/python main.pyThere's no bundled remote-deploy pipeline — how you get code onto the Pi is up to you. The supported path is to update the checkout on the Pi and re-run the installer:
cd ~/piclock && git pull
sudo scripts/install.sh # idempotent: re-syncs /opt/piclock, refreshes the venv, restartsinstall.sh re-syncs /opt/piclock, rebuilds the clocker venv, and restarts the service.
Pass the same feature flags you used originally (--homekit, --schedule, --reboot) — each
flag fully toggles its feature, so omitting one on a later run disables it.
If you'd rather push-to-deploy from CI, deploy/ has an adaptable Gitea Actions template — see deploy/README.md.
Everything except 3d_print/ is released into the public domain under The Unlicense — do whatever you like with it. The 3D-printable case in 3d_print/ is adapted from a CC-licensed Printables design and is instead CC BY-NC-SA 4.0 (attribution, noncommercial, share-alike); see 3d_print/LICENSE.md.