Skip to content

code-a-saurus/PiClock

Repository files navigation

PiClock

tests

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 finished PiClock on a nightstand: red 7-segment digits reading 12:21 in a black 3D-printed case On the workbench: a Raspberry Pi Zero wired to the display seated in the case, showing 5:46, with printed case parts and the bare 7-segment module alongside

How it stays correct

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's zoneinfo applies 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.

Hardware

  • 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/.

Software

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)

Setup (on the Pi)

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.sh

The 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.py

Verify the display is on the bus (once the hardware is attached):

sudo apt install -y i2c-tools && i2cdetect -y 1   # expect a device at 0x70

Configuration

Set 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, 015
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, 015
PICLOCK_NIGHT_BRIGHTNESS 0 Level the schedule's night dim applies, 015 (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.

Service

sudo systemctl status piclock
sudo systemctl restart piclock
journalctl -u piclock -f

One-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-15

install.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).

Development & tests

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.py

Updating

There'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, restarts

install.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.

License

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.

About

A Raspberry Pi Zero W bedside clock appliance: HT16K33 7-segment display driven directly over I2C, NTP/DST-correct, with systemd packaging, HomeKit, and a printable case.

Topics

Resources

License

Stars

6 stars

Watchers

0 watching

Forks

Contributors