DEV Community

Ibrahim Nauman
Ibrahim Nauman

Posted on

Switching Outdoor LED Lights at Sunset With Python, and Surviving Load-Shedding

We sell LED lighting in Pakistan, and a surprising number of the questions we get from customers who write software for a living aren't about the fittings at all. They're about switching. Someone has put a smart relay behind the gate light or the shop signboard, set a fixed 6 pm timer, and is now annoyed that the LED lights come on in broad daylight in June and well after dark in December.

This post is the small Python service we'd suggest instead. It switches outdoor LED lights relative to the actual sunset for your city, recovers from load-shedding without any special handling, and writes a log you can turn into a rough figure for what the lights cost to run.

Why a fixed timer drifts
Pakistan doesn't use daylight saving time, so the clock stays put while sunset moves. In Lahore, sunset falls a little after 5 pm in late December and after 7 pm in late June. That's a swing of around two hours. A fixed timer is wrong for most of the year, either burning power in daylight or leaving the gate's LED lights off for the first part of the evening.

Cities differ as well. Karachi sits further west and further south than Lahore, and in winter its sunset comes roughly three quarters of an hour later. A timer copied from a Lahore setup will be visibly off in Karachi.

Compute the state, don't schedule the event
The obvious design is an event scheduler: at sunset, send "on". The trouble is what happens when the event is missed. If the electricity is off at sunset, the Raspberry Pi running your script is off too, the event never fires, and the lights stay dark after power returns.

The fix is to make the code stateless about time. Every minute, ask one question: given the current time and today's sun times, should this relay be on? Then send that state, whether or not it has changed. A missed minute doesn't matter. A reboot doesn't matter. An outage at exactly sunset fixes itself one loop after the supply comes back.

The script
It needs two packages: astral for the sun calculations, and tzdata so that zoneinfo works on Windows machines too.

pip install astral tzdata
Here's the whole service. Change the coordinates to your own site and the IP addresses to your relays.

import csv
import datetime as dt
import time
import urllib.request
from zoneinfo import ZoneInfo

from astral import LocationInfo
from astral.sun import sun

PKT = ZoneInfo("Asia/Karachi")
SITE = LocationInfo("Lahore", "Pakistan", "Asia/Karachi", 31.52, 74.36)
ON_OFFSET = dt.timedelta(minutes=-10) # switch on a little before sunset

RELAYS = {
"gate": {"ip": "192.168.1.50", "off_at": None}, # dusk to dawn
"signboard": {"ip": "192.168.1.51", "off_at": dt.time(23, 0)}, # off at 11 pm
}

def desired_state(now, off_at):
"""True if the relay should be on at this moment."""
s = sun(SITE.observer, date=now.date(), tzinfo=PKT)
on_time = s["sunset"] + ON_OFFSET
if off_at is None:
return now >= on_time or now < s["sunrise"]
off_time = dt.datetime.combine(now.date(), off_at, tzinfo=PKT)
return on_time <= now < off_time

def set_relay(ip, on):
"""Send the state to a Tasmota relay over HTTP. Safe to repeat."""
cmd = "Power%20On" if on else "Power%20Off"
with urllib.request.urlopen(f"http://{ip}/cm?cmnd={cmd}", timeout=5) as r:
return r.status == 200

def log(name, on, ok):
stamp = dt.datetime.now(PKT).isoformat(timespec="seconds")
with open("lights_log.csv", "a", newline="") as f:
csv.writer(f).writerow([stamp, name, int(on), int(ok)])

def main():
last = {}
while True:
now = dt.datetime.now(PKT)
for name, cfg in RELAYS.items():
want = desired_state(now, cfg["off_at"])
try:
ok = set_relay(cfg["ip"], want)
except OSError:
ok = False
if last.get(name) != (want, ok):
log(name, want, ok)
last[name] = (want, ok)
time.sleep(60)

if name == "main":
main()
The relays in this example run Tasmota firmware, which accepts commands over plain HTTP, so the standard library is enough. If your devices sit on MQTT instead, swap set_relay for a paho-mqtt publish and leave the rest alone.

Two rules are built in. The gate light runs dusk to dawn. The signboard comes on ten minutes before sunset and goes off at 11 pm, after the market has closed. Adding a third relay is one more line in the dictionary.

What happens during load-shedding
Walk through a typical winter evening. Power goes at 6:30 pm, after sunset. The relays, the router and the Pi all go dark. At 7:30 pm the supply returns. The Pi boots, the loop runs, desired_state says the gate should be on, and the command goes out. Within a minute or two of the router reconnecting, everything is back where it should be. No catch-up logic, no missed-event queue.

Two settings still matter. The first is the relay's own behaviour at power-up. Tasmota's PowerOnState setting decides whether a relay comes back on, off or in its last saved state. For a security light, coming back on is the safer default in case the Pi is slow to recover. The second is the split between UPS and mains. If the Pi is on the house UPS and the relays aren't, the script keeps running through the outage and its commands fail, which is why the log records whether each command succeeded and not just what it asked for.

Turning the log into an estimate for your LED lights
The CSV gets one row whenever a relay's wanted state or its success flag changes. Pair each successful "on" with the next "off" for the same relay and you have hours on. Multiply by the rated wattage of the fittings on that relay and you get kWh, which you can hold up against the units on your WAPDA or K-Electric bill.

import csv
import datetime as dt
from collections import defaultdict

WATTS = {"gate": 50, "signboard": 100} # rated watts on each relay

on_since, hours = {}, defaultdict(float)
with open("lights_log.csv", newline="") as f:
for stamp, name, on, ok in csv.reader(f):
t = dt.datetime.fromisoformat(stamp)
if on == "1" and ok == "1":
on_since.setdefault(name, t)
elif on == "0" and name in on_since:
hours[name] += (t - on_since.pop(name)).total_seconds() / 3600

for name, h in hours.items():
print(f"{name}: {h:.1f} h on, about {h * WATTS[name] / 1000:.2f} kWh")
Treat the output as an estimate. It assumes the fittings draw their rated wattage, and it counts time when a relay was meant to be on but the supply itself was out. For a real figure, put an energy meter on the circuit for a week and compare the two numbers.

Hardware notes before you wire anything
Check the relay's contact rating against the load. LED drivers draw a brief surge at switch-on, and several outdoor LED lights on one small smart relay can weld its contacts. For more than a light or two, let the relay drive a contactor.
Mains wiring is a job for a qualified electrician. The code is the easy part.
Put outdoor relays in a sealed enclosure. Dust gets into everything during the May andhi season, and the monsoon finds any gap that's left.
Tasmota has its own sunrise and sunset timers if you only have one device. The Python version earns its place once you have several relays, want one log, or need different rules per light.
If you're choosing fittings for a setup like this, our product listings, each named with its rated wattage give you the numbers the WATTS table needs.

And if you build something along these lines, we'd like to hear how it went. You can reach the engineering team through Hyundai LED.

Top comments (0)