Tuesday 12 August 2014

A Better "Automatic Light" Controller

This may be one of the most ridiculus, unnecessary projects I've ever got involved with.

But sometimes when an idea takes hold, it becomes difficult to shake off.

The idea is to calculate sunset using a SINE function, and use this as the basis for controlling an outside light!

How did I get drawn into this?

Some Background

I decided to test a Pi Mote power controller by replacing an electro-mechanical timer (which was controlling an outside light), and writing a simple Gambas program to turn the light on at a suitable time. This program basically checked the current month and then selected a suitable turn-on time using a simple Select Case code segment something like this:-

    'OFF_TIME = 3am
    blnPowerOn = False
    Select Case Month(Now)
      Case 1 Or 12
        If Hour(Now) < OFF_TIME Or Hour(Now) > JAN_ON Then
          blnPowerOn = True
      Case 2 Or 11
        If Hour(Now) < OFF_TIME Or Hour(Now) > FEB_ON Then
          blnPowerOn = True
      Case 3 Or 10
        If Hour(Now) < OFF_TIME Or Hour(Now) > MAR_ON Then
          blnPowerOn = True
      Case 4 Or 9
        If Hour(Now) < OFF_TIME Or Hour(Now) > APR_ON Then
          blnPowerOn = True
      Case 5 Or 8
        If Hour(Now) < OFF_TIME Or Hour(Now) > MAY_ON Then
          blnPowerOn = True
      Case 6 Or 7
        If Hour(Now) < OFF_TIME Or Hour(Now) > JUN_ON Then
          blnPowerOn = True
    End Select
'turn light on or off

This system ran happily for a few weeks, during which time we had 2 power-cuts. Summer power-cuts are quite unusual in our district, but we get plenty during the winter.

Now it just so happens that a younger version of me installed the original light timer in a cramped and awkward position in the tiny loft above our kitchen. Now that I'm much older and less agile, I really hate climbing into the loft, and crawling on hands and knees to this dusty corner to reset the timer.

But there was no need this time, because my RaspberryPi just re-booted when the power came back up, and picked up the correct time from the internet.

So I decided my short-term test may as well keep running. When the winter comes, I can laugh in the face of power-cuts....Ha-ha-ha!

You couldn't just leave it there, could you!

However, from time-to-time I'd look out of my window and think "That light is just a bit early/late coming on tonight". The controller was working perfectly, but my program control based upon the current month was just not fine enough. So while the days grow shorter as we get further into August, I feel the need to control the turn-on time more closely.

I considered several possible improvements, such as using the week number instead of the month, or doing a look-up on a website which has sunset data.

I also downloaded a data table containing sunrise/sunset times, put it into a spreadsheet, and plotted day length.

Day length plot for south-east England

The plot, unsurprisingly, looked like a sine wave. As the earth tilts on its axis, the rate of change slows right down as it prepares to start tilting the other way.

So rather than having lots of numbers to look-up, it occurred to me to use an expression to generate the curve.

Now I'm no mathematician (come to that, I'm no programmer either) but I seem to remember that a sine-wave can be represented/plotted by a rotating vector. The vector starts horizontal (0 degrees) and rotates clockwise, becoming fully vertical at 90 degrees (sine wave maximum) and as it continues round to 270degees the sine wave passes back through the x-axis to reach its maximum negative value.

So by scaling the days of the year into degrees (e.g. 360/365) it should be possible to calculate the change in daylight by day using "SIN(a)" where "a" is the angle starting at the first day. Actually, the first day needs to be the spring equinox, the 20th March.

Sine-wave starts on day 79 (spring Equinox)

As the sine of 0deg = 0 and sine of 90deg = 1 we can scale daylength by using the difference in daylength between the summer solstice (21st June) and the spring equinox (20th March). We then need to apply an offset equal to the daylength on the 20th March. Otherwise we would have no daylight at all on 20th March, and negative daylight during the winter months, a bit like Scotland.

See Wikipedia for an explanation/definition of the equinox.

We need to express the angle in Radians, so in Gambas we use SIN(RAD(degrees))

So far we have:-
Scale days = 360/(365+leapDay)
Constant OFFSET_DAYS = 20th March = day 0 (so offset = 79 days)
Constant DAYLIGHT_EQUINOX  (i.e. daylight for 20th March)
Constant DAYLIGHT_21st_JUNE

...and (very) basically:-
Daylength = DAYLIGHT_EQUINOX + (SIN(RAD(scaledDay)) * DAYLIGHT_21st_JUNE)

...and sunset (today) is calculated from: DAYLIGHT_EQUINOX + (DayLengthToday - DAYLIGHT_EQUINOX)/2

There are quite a few errors in these simple calculations. For example our rotating vector should rotate 90deg from the spring equinox to the summer solstice. So this should happen in 365/4 = 91 & a bit days. But there are 93 days separating the 20th March and the 21st June.

The sunrise occurs when the top edge of the sun peeks over the horizon, and sunset when the top edge disappears. So this extends the day by the time taken for the suns diameter to pass below the horizon.

The Great British Summer Time

My sunrise/sunset data is based on Greenwich Mean Time (GMT).  But this gets screwed for 6 months of the year by daylight saving, when we have to change the clocks to British Summer Time (BST).

So I need to know when daylight saving kicks in and changes the concept of local time on my RaspberryPi. As the Gambas function "System.TimeZone" does not appear to work as expected, I've had to code it like this:-

Public Function LastSundayInMonth(intMonth As Integer) As Integer
  Dim index As Integer
  Dim intLastSunday As Integer
  For index = 1 To 7
    'find 1st Sunday
    If WeekDay(intMonth & "/" & index & "/" & Year(Now)) = gb.Sunday Then
      'calculate last Sunday
      intLastSunday = index + (4 * 7)
      If intLastSunday > 31 Then
        intLastSunday = index + (3 * 7)
  Return intLastSunday

...then I can call this function to check if we are on GMT or BST something like this:-

  'Check GMT/BST
  Select Case Month(Now)
    Case 1 Or 2 Or 11 Or 12  'winter months
    intTimeStandard = GMT
    Case 4 To 9 'summer months
    intTimeStandard = BST
    Case 3

      If Day(Now) < LastSundayInMonth(3) Then
        intTimeStandard = GMT
        intTimeStandard = BST
    Case 10

      If Day(Now) < LastSundayInMonth(10) Then
        intTimeStandard = BST
        intTimeStandard = GMT

  End Select

Leap Years

To check for leap years I could probably just test that the year is divisible by 4, but I don't want to be blamed for any future "year 2K1 or 3K" problems:-

Public Function CheckLeapYear() As Integer
'Check for leap-year
Dim iLeap As Integer

  iLeap = 0
  If Year(Now) Mod 4 = 0 And Year(Now) Mod 100 > 0 Then
    iLeap = 1
  'I wont live long enough to worry about this...
  If Year(Now) Mod 4 = 0 And Year(Now) Mod 100 = 0 And Year(Now) Mod 400 = 0 Then
    iLeap = 1
  Return iLeap


We still have a concept in Britain of "lighting up time" in recognition that there is often plenty of light when the sun is hiding just below the horizon. It use to be a legal requirement to turn your car lights on within 1 hour of sunset. I think this was updated to within 30 minutes during the mid 1950s.

Either way, I'm including a Twilight constant which will be added to the system "TurnOnTime" to delay switch on until its more or less "dark". This has been initially set to 45 minutes.


The benefit of this system to me over an electro-mechanical timer for our back-yard light is that it recovers after a power cut. I do save a bit of power, if you accept that this Raspberry Pi is running all the time anyway. However, my outside lights are only 8W each, so there is not a significant saving by delaying turn-on time in the summer compared to winter months.

The benefit of this system over an auto-light/dark bulb (we have one of these over the front door, which switches itself on after dark) is that when we reverse the car into the driveway, the reversing light is bright enough to turn off the auto-light/dark bulb. This wont recover until its either seen a light/dark cycle or we power cycle the unit. So just when we need the light on, we are thrust back into darkness.

Convinced? ...well I had to at least try to justify it.


  1. Just commenting to let you know I got this very idea working using the Python "ephem" module which allows for astronomical calculations eg. sunrise and sunset can be calculated for the current day, based on latitude, longitude and elevation. In my case, it is saving me a ton load of money as I have the Energenie sockets hooked into 2 oil heaters in the conservatory. They now only come on at night, going off during the day.

    Pretty pleased with the result. Do not have to rely on light-sensitive switches or any of that nonsense; all nicely calculated and automatically adapts to the changing daylight period throughout the year. :)

    1. Sounds like a great application for this idea (...and there seem to be Python libraries for just about anything).

      My automatic light just runs without any attention...except when the clocks changed last March. I had a silly bug in my program, and the light began coming on earlier and earlier each day. But its all right now.