This document is LLM-generated.
Steps to set up the software from scratch on a Pi. Hardware build is separate (see HARDWARE_RUNBOOK.md).
Facts: Raspberry Pi OS Lite (Debian 13, Python 3.13). A reference unit is a Zero W (armv6) or
a Zero 2 W (armv7l/aarch64). Set the timezone once with timedatectl; time comes from NTP via
systemd-timesyncd (the stock public pool by default — pin your own server only if you want to,
see §2). The app runs as the unprivileged clocker account from /opt/piclock.
sudo apt install git i2c-tools unattended-upgrades
sudo raspi-config nonint do_i2c 0 # enable I2C -> /dev/i2c-1
sudo timedatectl set-timezone America/New_York # use your own zone
timedatectl # confirm: NTP service active, clock synchronizedunattended-upgrades (usually preinstalled) keeps the OS current on its own — including
tzdata, whose updated DST rules are half of the clock's never-touch-it story.
Logging is ephemeral by default. Raspberry Pi OS ships journald with Storage=volatile
(the journal lives in RAM and is lost on reboot/power loss), which spares SD-card writes. PiClock
leaves that choice to you — it ships no journald config. If you want log history to survive
reboots, create /etc/systemd/journald.conf.d/99-piclock-persistent.conf with:
[Journal]
Storage=persistent
SystemMaxUse=200M
SystemMaxFileSize=20MThen apply it:
sudo systemctl restart systemd-journald && sudo journalctl --flush
journalctl --disk-usage # confirm it's now under /var/log/journalUse a high-endurance SD card if you keep logs on disk.
Onboard green LED off (optional). Raspberry Pi Zero, Zero W, and Zero 2 W expose one
green power/activity LED. The sysfs LED name can vary by OS/kernel; on one current reference
Pi it appears as /sys/class/leds/ACT. Check yours first:
ls -la /sys/class/ledsIf the onboard LED is ACT, create /etc/systemd/system/piclock-led-off.service with:
[Unit]
Description=PiClock: turn off onboard green LED
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'if [ -e /sys/class/leds/ACT/trigger ]; then echo none > /sys/class/leds/ACT/trigger; fi'
ExecStart=/bin/sh -c 'if [ -e /sys/class/leds/ACT/brightness ]; then echo 0 > /sys/class/leds/ACT/brightness; fi'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetThen enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now piclock-led-off.service
cat /sys/class/leds/ACT/brightness # expect 0Do not also add old act_led_* lines to /boot/firmware/config.txt; on some Zero 2 W setups
those can prevent the sysfs brightness method from taking effect.
Get a checkout of this repo onto the Pi
(git clone https://github.com/code-a-saurus/PiClock.git, scp, etc.), then from it:
sudo scripts/install.shThe installer takes three optional feature 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 (§4).--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 — §5).--reboot— enables a monthly midday reboot that returns the appliance to a known-good state (§6).
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.
Creates clocker (system user, i2c group, no sudo), creates the clockctl group (gates
the piclockctl control socket — see §3), deploys to /opt/piclock, builds the venv,
installs + enables piclock.service, installs piclockctl to /usr/local/bin, and
installs a timesyncd drop-in. That drop-in
(systemd/timesyncd-piclock.conf) is a no-op by default —
it keeps the stock NTP pool, which is all the clock needs. To pin a specific (e.g. LAN) NTP
server, edit it and re-run. Idempotent.
Confirm the clock is syncing:
timedatectl # System clock synchronized: yes
timedatectl show-timesync -p ServerName -p ServerAddressThe service is left enabled; start it once the display is wired:
sudo systemctl start piclocksudo systemctl {start,stop,restart,status} piclock
journalctl -u piclock -f
i2cdetect -y 1 # confirm display at 0x70 (needs i2c-tools)One-off display commands via piclockctl (installed to /usr/local/bin by install.sh;
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-15The commands travel over a Unix socket (/run/piclock/ctl.sock, mode 0660) gated by the
clockctl group, which install.sh creates. Add users who need to run clock commands:
sudo usermod -aG clockctl <user> (log out/in to pick it up).
Config via PICLOCK_* env (in the unit): PICLOCK_24H=1, PICLOCK_BRIGHTNESS=0..15,
PICLOCK_BLINK=0/1, PICLOCK_SYNC_DOT=0/1 (lower-left dot lit while NTP-synced, dark when
free-running; on by default), PICLOCK_WIFI_DOT=0/1 + PICLOCK_WIFI_IFACE=wlan0
(upper-left dot lit while the Wi-Fi link is up, polled from sysfs every tick; on by
default), PICLOCK_TROUBLE_DOTS=0/1 (1 inverts both dots into trouble lights — dark
while nominal, lit only on stale/unsynced NTP or a down link; off by default), PICLOCK_DISPLAY=null (run without hardware), and
PICLOCK_WAKE_BRIGHTNESS / PICLOCK_NIGHT_BRIGHTNESS (0-15 levels the optional day/night
schedule applies at its wake and dim edges — see §5; the schedule's on/off switch and times
live in its drop-in, also §5). The unit sets
PICLOCK_BRIGHTNESS=0 (dimmest); 24h and steady colon by default. Unit changes apply by
re-running install.sh (or a manual copy + daemon-reload). No display attached → logs
"display unavailable", retries every 10s, no crash.
Exposes the clock to Home.app / Siri as a Lightbulb named "Clock": On/Off blanks or shows the time (to spare the 7-seg's life), Brightness maps to the HT16K33 level. Off by default; the core appliance needs none of this.
Enable on the Pi — from a repo checkout (as in §2), re-run the installer with the flag:
sudo scripts/install.sh --homekit # installs the extra deps + drop-in, restarts
journalctl -u piclock | grep 'pairing PIN' # note the xxx-xx-xxx code--homekit installs requirements-homekit.txt into the clocker venv and drops in
/etc/systemd/system/piclock.service.d/homekit.conf (the StateDirectory + PICLOCK_HOMEKIT
env). Re-running install.sh without the flag removes the drop-in, disabling HomeKit.
Pair: iPhone → Home app → + → Add Accessory → "More options" (it's an uncertified DIY accessory) → pick "Clock" → enter the PIN from the journal. Then "Hey Siri, turn off the Clock" / "...turn on the Clock". For a custom phrase like "clock off", add a Home automation or Shortcut.
Caveats:
- The zeroconf pin is architecture-selected in
requirements-homekit.txtvia PEP 508 markers, so pip installs the right one automatically — no install flag. On armv6 (Pi Zero W) it's pinned to 0.39.4, the last pure-Python release; newer versions have no armv6 wheel (piwheels builds armv7 only). On armv7l/aarch64 (Zero 2 W and up) it uses the current release. HAP-python 5.0.0 works against both. Don't bump the armv6 pin without re-checking armv6 wheels. - Pairing keys persist in
/var/lib/piclock/homekit.state(viaStateDirectory=). To re-pair from scratch, stop the service and delete that file. - mDNS: Raspberry Pi OS already runs
avahi-daemon; python-zeroconf coexists with it on UDP 5353.
Blanks the 7-seg during the day and shows it in the evening to spare the display, while the service keeps running so HomeKit/Siri can still turn it on on demand. The schedule runs inside the app (piclock/schedule.py): each loop tick computes which window "now" falls in — blank from 08:00, show at the wake brightness from 17:00, dim to the night brightness from 20:00 (defaults; times are in the Pi's local timezone) — and applies it only when the window changes. Because the change is edge-triggered, a HomeKit/Siri override sticks until the schedule's next on/off edge — and the dim edge only adjusts brightness, so turning the display off in the evening keeps it off overnight. Because the first tick after startup counts as an edge, a reboot or deploy mid-window always comes up in the correct state on its own. There are no schedule timers or helper units.
Enable on the Pi (from a repo checkout, as in §2):
sudo scripts/install.sh --schedule # add --homekit too for both
journalctl -u piclock | grep 'schedule:' # see the phase it applied at startup--schedule installs /etc/systemd/system/piclock.service.d/schedule.conf (from
systemd/piclock-schedule.conf), which sets
PICLOCK_SCHEDULE=on. Independent of HomeKit — works with or without it. Re-run
install.sh without --schedule to remove the drop-in and disable the schedule. To force a
state by hand, use the Home app/Siri (with HomeKit enabled) — the schedule will re-assert
itself at its next edge.
The scheduled wake always brings the display up at the wake brightness (default 15,
full), regardless of the brightness last set by Siri. The scheduled dim drops it to the
night brightness (default 0, the dimmest lit level; the HT16K33 has only 16 brightness
steps). Both levels are tunable via PICLOCK_WAKE_BRIGHTNESS / PICLOCK_NIGHT_BRIGHTNESS in
the unit (see §3). When HomeKit is enabled, schedule-driven on/off and those brightness
resets are pushed back to the Home app, so its tile stays in sync.
Change the times by setting PICLOCK_OFF_AT / PICLOCK_ON_AT / PICLOCK_DIM_AT (HH:MM)
in the schedule drop-in — the commented defaults are already there — then re-run
install.sh --schedule (or systemctl daemon-reload && systemctl restart piclock after a
manual edit). The three times must be in daily order (off < on < dim); the app refuses to
start with a misordered schedule rather than guessing.
A monthly reboot returns the appliance to a known-good state and keeps uptime bounded — the
clock already recovers unattended from power loss, so this is routine hygiene, not a fix for
anything. piclock-reboot.timer fires *-*-01 12:00 local; noon sits inside the display
off-window, so the post-reboot Wi-Fi/NTP resync (display shows ----) is invisible, and you're
awake if it ever misbehaves (the display stays blank until 17:00 regardless, leaving hours of
margin). The timer is not Persistent= — a just-booted Pi is already fresh, so a missed
slot is skipped rather than rebooting right after boot. The reboot oneshot runs journalctl --sync first so the latest entries are flushed before we go down (relevant only if you've
enabled a persistent journal — see §1; with the default volatile journal there's nothing to keep).
sudo scripts/install.sh --reboot # install + enable the timer
systemctl list-timers piclock-reboot.timer # confirm next runRe-run install.sh without --reboot to disable + remove it. Change the cadence by editing
OnCalendar= in piclock-reboot.timer, then re-run install.sh --reboot.
There is no bundled remote-deploy pipeline; deployment is your call. The supported path is to update the checkout on the Pi and re-run the installer:
cd ~/piclock && git pull
sudo scripts/install.sh # re-pass any feature flags you useinstall.sh re-syncs /opt/piclock, rebuilds the clocker venv, and restarts the service.
It's idempotent. If you build your own push-to-deploy on top, run it as an unprivileged account
that calls install.sh (or a narrower apply step). An adaptable Gitea Actions template doing
exactly that lives in deploy/ — see deploy/README.md.
python3 -m venv venv && venv/bin/pip install -r requirements-dev.txt
venv/bin/pytest
PICLOCK_DISPLAY=null venv/bin/python main.py
# HomeKit off-Pi: add the extras, then run with the null display (no hardware)
venv/bin/pip install -r requirements-homekit.txt
PICLOCK_DISPLAY=null PICLOCK_HOMEKIT=on venv/bin/python main.pyThe HomeKit unit tests cover only the pure parts (control state, brightness mapping) and run
without the extras installed — pyhap is imported lazily inside start_homekit.