last updated for: Raspberry Pi OS Bookworm (2025) — Kernel 6.1.x
Shelter Light Control System is a standalone, autonomous lighting controller designed for bicycle shelters and similar outdoor environments. It uses a GPS module and activity detection inputs to intelligently schedule lighting during hours of darkness, based on historical activity patterns and sunrise/sunset times.
The system is designed to operate completely offline and headless (no display, no network), learning and adapting over time without external input. Configuration updates and system logs can be managed via USB device insertion.
The system runs on a Raspberry Pi Zero (or similar) and is built using Python 3 with minimal external dependencies.
The Shelter Light Control System is designed to run efficiently on a Raspberry Pi Zero (or similar) with a minimal, headless configuration.
TLDR – Read TLDR.md
dd
to write the OS image to your microSD card.ssh
in the /boot
partition to allow remote access for debugging.or once booted through sudo raspi-config
Advanced Options -> SSH
Expanding ensure the system has access to the full size of the SD Card. This is done by:
sudo raspi-config
sudo reboot
Full use of the SD Card can be verified by checking the root partition (\
) is the same size as the SD card
After reboot:
df -h
Will output something like:
Filesystem Size Used Avail Use% Mounted On
/dev/root 29G 1.8G 26G 7% /
The application is written to calculate and control using UTC as the default timezone throughout the year. This avoids complications with timezone boundaries and daylight saving. Make sure the RPi has its timezone set to UTC:
sudo raspi-config
Choose Localisation Options -> Timezone. At the bottom of the list of available timezones the option ‘None of the above’ exists, choose this and then ‘UTC’
The GPS Module is intended to be connected to the hardware serial of the RPI (Header pins 8 & 10 which correspond to GPIO 14 & 15 - UART TX & UART RX)
sudo raspi-config
Choose Interface Options -> I6 Serial Port, choose “NO” when asked if you would like a login shell accessible over serial and “YES” when asked if you would like serial port hardware to be enabled
RPi Zero
On the Raspberry Pi Zero, the UART (serial) interface is shared with Bluetooth by default. This can cause conflicts if your GPS module is connected to the UART TX
and UART RX
pins.
To disable Bluetooth and free up the serial interface for your GPS:
Open the file /boot/firmware/config.txt
with root privileges:
sudo nano /boot/firmware/config.txt
On Raspberry Pi os versions earlier than bookworm, this file resides in /boot/config.txt
Scroll to the bottom and add the following line:
dtoverlay=disable-bt
Save and exit (Ctrl+X
, then Y
, then Enter
).
Reboot the Pi:
sudo reboot
After rebooting, the serial port will be dedicated to your GPS module.
Certain system services are not required and should be disabled to improve boot time and reduce resource usage.
See the section Disabling Unnecessary Services later in this document for recommended services to disable.
sudo apt update
sudo apt install -y python3 python3-venv python3-pip python3-lgpio python3-dev libpq-dev postgresql libopenblas-dev build-essential git
Why are these packages needed?
Package | Needed for |
---|---|
python3, python3-venv, python3-pip | Core Python installation |
python3-lgpio | GPIO Control library (replaces RPi.GPIO) |
python3-dev | Headers for building Python C extensions (critical for psycopg2, etc.) |
libpq-dev | PostgreSQL C client libraries needed for psycopg2. |
postgresql | Activity database backend. |
libopenblas-dev | Needed by LightGBM for fast matrix math |
build-essential | Compiler toolchain (gcc, g++, make) needed for pip installing C/C++-based packages. |
git | For pulling code if needed. |
/usr/bin/tvservice -o
The Shelter Light Control System uses a local PostgreSQL database to store historical activity logs and generated light schedules.
Database connection settings are defined in config.ini
under the [ACTIVITY_DB]
section.
Example configuration:
[ACTIVITY_DB]
host = localhost
port = 5432
database = activity_db
user = pi
password = changeme
connect_retry = 3
connect_retry_delay = 5
Run the following commands:
sudo -u postgres psql
Inside the psql
shell:
CREATE DATABASE activity_db;
CREATE USER pi WITH ENCRYPTED PASSWORD 'changeme';
GRANT ALL PRIVILEGES ON DATABASE activity_db TO pi;
\c activity_db
GRANT USAGE ON SCHEMA public TO pi;
GRANT CREATE ON SCHEMA public TO pi;
\q
The system will automatically create the required tables (activity_log
and light_schedules
) when it first runs.
Database username and password must first be set in the config
Alternatively, to create manually:
Connect to the database
psql -U pi -d smartlight
Paste the table creation SQL statements
-- Create activity_log table
CREATE TABLE IF NOT EXISTS activity_log (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
day_of_week SMALLINT NOT NULL,
month SMALLINT NOT NULL,
year SMALLINT NOT NULL,
duration SMALLINT NOT NULL,
activity_pin SMALLINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_activity_timestamp
ON activity_log (timestamp);
-- Create light_schedules table
CREATE TABLE IF NOT EXISTS light_schedules (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
interval_number SMALLINT NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
prediction BOOLEAN NOT NULL,
was_correct BOOLEAN,
false_positive BOOLEAN DEFAULT FALSE,
false_negative BOOLEAN DEFAULT FALSE,
confidence DECIMAL(5,4),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT valid_confidence CHECK (confidence >= 0 AND confidence <= 1),
CONSTRAINT unique_schedule_interval UNIQUE (date, interval_number)
);
CREATE INDEX IF NOT EXISTS idx_light_schedules_date
ON light_schedules(date);
CREATE INDEX IF NOT EXISTS idx_light_schedules_interval
ON light_schedules(interval_number);
-- Create update trigger for updated_at column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS update_light_schedules_updated_at ON light_schedules;
CREATE TRIGGER update_light_schedules_updated_at
BEFORE UPDATE ON light_schedules
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
\q
Stored Procedures If the database is automatically setup by the application during its first run, the stored procedures described in - Stored_procedures.md are also created —
This system is written Python 3 and is intended to run on Raspberry Pi Zero (or similar) with python version >= 3.11.
Grab the source for the application from the git repository with:
git clone https://WillBickerstaff/shelterlight.git
The following Python packages are required:
numpy
— Use the --prefer-binary
flag to avoid compiling on the Pi, which is very slow and prone to errors:pip install --prefer-binary numpy
RPi.GPIO
— Used for light control output and other GPIO until full migration to lgpio is completepyserial
— Serial communication for GPS modulelightgbm
— Machine learning for schedule predictionpsycopg2
— PostgreSQL database driverpandas
— Data manipulationtimezonefinder
— Determine location timezoneSQLAlchemy
— pandas SQLAlchemy interfaceNote: Although all scheduling and system operations use UTC internally, timezonefinder
is retained to support future features such as a local display. This would allow sunrise, sunset, and schedule times to present in a human readable local time to avoid confusion.
A complete list is provided in req_modules.txt
:
To avoid affecting your system Python environment, it is recommended to install dependencies in a virtual environment. use --system-site-packages
to allow the virtual environment access to the system lgpio library
cd ~/shelterlight
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install -r req_modules.txt
Configure to run automatically at boot using systemd.
Create a new systemd service file:
sudo nano /etc/systemd/system/shelterlight.service
Paste the following content:
[Unit]
Description=Shelter Light Controller
After=network.target
[Service]
ExecStart=/home/pi/shelterlight/.venv/bin/python /home/pi/shelterlight/shelterlight.py
WorkingDirectory=/home/pi/shelterlight
Restart=on-failure
RestartSec=5
User=pi
Group=pi
[Install]
WantedBy=multi-user.target
Run the following commands to enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable shelterlight.service
sudo systemctl start shelterlight.service
sudo systemctl status shelterlight.service
With the service configured:
Restart=on-failure
RestartSec=5
The service will automatically restart 5 seconds after any unexpected failure.
To view runtime logs:
journalctl -u shelterlight.service -f
sudo systemctl stop shelterlight.service
sudo systemctl disable shelterlight.service
sudoers
permissions.The system uses the GPS module as the authoritative time source. After a successful fix, the system clock is updated to GPS-provided UTC time.
After a valid fix, the system executes:
sudo /bin/date -s <utc_time>
This will synchronise the Raspberry Pi system time to UTC.
Setting system time requires sudo privileges.
Create a sudoers rule:
sudo visudo -f /etc/sudoers.d/shelterlight
Add:
pi ALL=(ALL) NOPASSWD: /bin/date
Disable NTP (Optional)
sudo timedatectl set-ntp false
The Raspberry Pi Zero does not have a battery-backed RTC. This system operates offline and uses GPS as the only reliable time source.
To prevent log files from consuming excessive disk space, it is recommended to configure logrotate for the shelter light log files.
This will ensure that old logs are compressed and automatically removed after a defined retention period.
Create a logrotate configuration file:
sudo nano /etc/logrotate.d/shelterlight
Example content:
/home/pi/shelterlight/shelterlight.log {
daily
rotate 7
compress
missingok
notifempty
delaycompress
copytruncate
}
Explanation:
Option | Description |
---|---|
daily |
Rotate the log file daily. |
rotate 7 |
Keep the last 7 rotated log files. |
compress |
Compress old log files to save space. |
missingok |
Do not raise an error if the log file is missing. |
notifempty |
Do not rotate if the log file is empty. |
delaycompress |
Compress the previous log file, not the current one. |
copytruncate |
Truncate the original log file after creating a copy. |
You can manually force logrotate to test your configuration:
sudo logrotate -f /etc/logrotate.d/shelterlight
By default, logrotate runs daily via cron on Raspberry Pi OS. No additional action is required after creating the config file.
When a USB device is inserted, all log files, including rotated and compressed logs, will be backed up to the USB drive. This includes:
shelterlight.log
shelterlight.log.1
shelterlight.log.2.gz
The backup files will be renamed to include an ISO-formatted timestamp:
/media/usb/smartlight/logs/shelterlight.log_backup_2025-03-31T14:23:07
/media/usb/smartlight/logs/shelterlight.log.1_backup_2025-03-31T14:23:07
/media/usb/smartlight/logs/shelterlight.log.2.gz_backup_2025-03-31T14:23:07
The backup operation happens once per USB insertion event. If the USB is removed and re-inserted, a fresh backup will be created.
To improve system performance, reduce boot time, and lower power consumption, certain default Raspberry Pi OS services can be safely disabled for this headless, offline application.
The following services are not required for the Shelter Light Control System and can be disabled:
Service | Reason |
---|---|
bluetooth.service | Bluetooth hardware is not used. |
hciuart.service | Bluetooth UART service, not needed. |
avahi-daemon.service | mDNS/DNS-SD service (Bonjour/ZeroConf), not used. |
triggerhappy.service | Listens for keyboard/mouse button events (special & media keys). Not needed in headless use. |
wpa_supplicant.service | Wi-Fi service. System runs offline with no network requirement. |
dhcpcd.service | DHCP client service. No network required. |
nfs-common.service | Network filesystem client. Not used. |
rpcbind.service | RPC service for NFS, not required. |
cups.service | Printing service, not required. |
Disable these services with:
sudo systemctl disable bluetooth.service
sudo systemctl disable hciuart.service
sudo systemctl disable avahi-daemon.service
sudo systemctl disable triggerhappy.service
sudo systemctl disable wpa_supplicant.service
sudo systemctl disable dhcpcd.service
sudo systemctl disable nfs-common.service
sudo systemctl disable rpcbind.service
sudo systemctl disable cups.service
If Wi-Fi or networking is required later (e.g., for debugging), to re-enable:
sudo systemctl enable wpa_supplicant.service
sudo systemctl enable dhcpcd.service
Service | Function |
---|---|
networking.service | Configures network interfaces (mainly ethernet & static IPs). Not needed unless you plan to connect to a network regularly and want easy control. |
network-manager.service | A tool for managing network connections (wired, WiFi, VPN, etc.). It handles dynamic networks, roaming, user-managed connections, etc. Not needed unless you plan to connect to a network regularly and want easy control. |
ssh.service | the SSH server (OpenSSH) allows you to remotely log in to the Pi from another machine. Disable SSH for security and simplicity. But if you want to maintain or debug the system remotely without physically accessing it, leave it enabled. |
Each of these can be disabled with:
sudo systemctl disable networking.service
sudo systemctl disable network-manager.service
sudo systemctl disable ssh.service
The following packages can be considered as candidates for complete removal from the system to further minimize disk usage, memory footprint, and boot times.
These packages are not required for operation of the Shelter Light Control system and are safe to purge if the system’s use case does not depend on them:
Package Group | Packages | Reason for Removal |
---|---|---|
Bluetooth Stack | pi-bluetooth , bluez , bluez-firmware |
Unused if no Bluetooth devices are required. |
Modem/PPP Support | modemmanager , ppp |
Dial-up or cellular modems are not used. |
Network Filesystem (NFS) | rpcbind , nfs-common , rpcsvc-proto |
No NFS file shares are required. |
Audio Stack | alsa-utils , alsa-topology-conf , alsa-ucm-conf |
No audio playback or input needed. |
Hotkey Event Daemon | triggerhappy |
Not required unless using extra keyboard buttons or hardware keys. |
Swap Management | dphys-swapfile |
Swapfile is not needed with sufficient RAM and light workload. |
Network Time Synchronization | systemd-timesyncd |
GPS module provides accurate UTC time; NTP synchronization is not used. |
sudo apt purge -y \
pi-bluetooth bluez bluez-firmware \
modemmanager ppp \
rpcbind nfs-common rpcsvc-proto \
alsa-utils alsa-topology-conf alsa-ucm-conf \
triggerhappy dphys-swapfile \
systemd-timesyncd
sudo apt autoremove --purge
sudo apt clean
The Shelter Light Control System is designed to run unattended. However, minimal periodic maintenance is recommended to ensure long-term reliability.
Task | Frequency | Reason | Commands / Actions |
---|---|---|---|
USB Backup Retrieval | Monthly or after unusual weather | Back up logs & configuration, and check for errors | Insert USB and check /media/usb/smartlight/logs/ |
Log File Storage Check | Quarterly | Ensure log rotation is working and storage space is healthy | sudo du -sh /home/pi/shelterlight/log sudo df -h / |
Database Health Check | Every 6 months | Verify database integrity and storage | sudo -u postgres psql -c "\l" psql -U pi -d smartlight -c "\dt" du -sh /var/lib/postgresql/14/main |
Hardware Inspection | Annually / after severe weather | Ensure cables, sensors & hardware are intact | Visual check |
SD Card Health | Annually | SD cards wear out over time (Consider cloning) | sudo smartctl -a /dev/mmcblk0 (Requires smartmontools) sudo apt install smartmontools |
Over time, the database may accumulate old activity and schedule data. It is recommended to periodically remove older records to prevent excessive disk usage.
Since the system is configured with password access, you can securely automate cleanup tasks by creating a .pgpass file:
nano /home/pi/.pgpass
Add the following line (match your config.ini credentials):
localhost:5432:smartlight:pi:changeme
Set the correct file permissions:
chmod 600 /home/pi/.pgpass
Edit the crontab for user pi:
crontab -e
Add the following line to remove old data monthly:
0 12 1 * * psql -U pi -d smartlight -c "DELETE FROM activity_log WHERE timestamp < NOW() - INTERVAL '90 days'; DELETE FROM light_schedules WHERE date < NOW() - INTERVAL '180 days';"
Activity data over 90 days old and schedules over 180 days old will be cleared out on the 1st of every month at midday.
PostgreSQL does not immediately free disk space when records are deleted. Instead, deleted rows are marked as “dead” and remain in the table until a VACUUM operation is performed.
If you want to physically reclaim disk space after cleanup:
psql -U pi -d smartlight -c "VACUUM FULL;"
Note:
Regular auto-vacuum is usually enabled by default and will handle cleanup over time.
VACUUM FULL will lock the tables until it completes, so it should only be run during maintenance periods when the service can tolerate downtime.
Log into PostgreSQL:
psql -U pi -d smartlight
Then run:
SHOW autovacuum;
Expected result:
autovacuum
-------------
on
(1 row)
If it says off, then autovacuum is disabled (which is rare on modern PostgreSQL). To enable, edit your postgresql.conf file (usually in /etc/postgresql/14/main/postgresql.conf or similar):
autovacuum = on
Then restart PostgreSQL:
sudo systemctl restart postgresql
The system uses an .ini
configuration file (config.ini
) to control behaviour, GPIO pin assignments, database connection, location fallback, GPS settings, and more. Default values are embedded in the system and automatically used if options are missing.
Full configuration documentation is provided in config_README.md
When the system detects that a machine-learned schedule has low confidence, it can optionally fall back to a safer alternative. This fallback behaviour is configured in the [FALLBACK]
section of config.ini
.
There are three fallback modes:
History
Reuses the most accurate previously generated schedule for the same weekday from within a configurable number of past days (history_days
).
If no suitable day is found, the system attempts to use a CSV-defined fallback schedule.
If both fail, the low-confidence schedule is used.
Schedule
Ignores history and attempts to build a schedule from a human-readable fallback file (schedule_file
).
If the file is invalid or missing, the low-confidence schedule is used.
None No fallback is applied. The schedule is used as-is, regardless of accuracy or confidence.
The fallback file is a CSV (default: Fallback_Schedule.csv
) that defines one or more light-on periods for each day. Each row specifies a day of the week (or ANY
), an on_time
, and a duration
in 24-hour LOCAL time. Any daylight saving time (DST) adjustments will be handled automatically.
day, on_time, duration
ANY, 01:30, 01:00
ANY, 05:30, 01:00
Monday, 18:00, 02:00
Note The times in the csv must be Local time.
The system converts these blocks into UTC-based interval predictions using the configured interval_minutes, applying the correct local timezone offset for the day (including DST). It then builds a complete daily schedule for fallback use.