Categories
How To Raspberry Pi

Astral — calculate sunrise, sunset, moon phases, golden / blue hour with Python

Now that I have taken a closer look at astral, I really like it, I must say. So far, I have been able to get the data for sunrise and sunset online. But since the calculations with Astral hardly differ from the online results, I prefer an offline solution.

I would like to emphasize again that the Raspberry Pi does not have an RTC (Real Time Clock). In order for the Raspberry Pi to set the correct time at startup, it must be online. Of course, you can set the clock manually or you can solder an RTC. The difference is that the Pi only needs to be online for a short time. Another advantage is that the Python script is much shorter and therefore clearer.

Since Astral works with Python 3, you are not limited to a Raspberry Pi. But the Pi is well suited for smart home projects or just to experiment with it.

Install Astral

On the Raspberry Pi, Astral is quickly installed via pip, provided that this is available. If not – install it. Here are the commands for both:

sudo apt install python3-pip
pip3 install astral

That’s it. Unlike in the desktop version, gpiozero is not installed on Raspbian Lite. Since I switch the GPIO pins with Astral at the end, I do this step right away:

sudo apt install python3-gpiozero

Now I am good to go. By the way, my Raspberry Pi is running Raspbian Buster.

My new script

I would like to show you the script I am using now – at least with the concept of it. It looks lie this:

from astral import LocationInfo
from astral.sun import sun
from datetime import datetime
from time import sleep
import pytz

utc=pytz.UTC

city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) # swap with your location

while True:

    s = sun(city.observer)
    time_now = utc.localize(datetime.now())
    sunrise = s["sunrise"]
    dusk = s["dusk"]

    if time_now > sunrise and time_now < dusk: # am I in between sunset and dusk?
        print("Daylight")
    else:
        print("Switch on light")

    sleep(60)

The script is very logical. In my case, it gets the data for sunrise and dusk. Then the program checks if the current time is between sunrise and dusk. If that is the case, it tells you that it is daylight — if not, it is dark.

This is an infinite loop that pauses for 60 seconds at the end of a run.

With my online solution I still checked the date and only made a query to the API when the date changed. I could do the same here, but the calculation is so fast that I don’t think it’s necessary.

My night light with pfFace Digital and Astral

Why I use a piFace Digital? Because I already have it and it was a birthday present — plus the thing works. Today I would rather use an Automation HAT, I think — or take a relay and switch it via GPIO.

My Raspberry Pi B with the piFace Digital now switches my night light as follows.

from astral import LocationInfo
from astral.sun import sun
from datetime import datetime
from time import sleep
import pifacedigitalio as p
import pytz

utc=pytz.UTC

pifacedigital = p.PiFaceDigital()
pifacedigital.output_pins[3].turn_on() # Schaltet bei einem Pi-Neustart an, ist nur ein Check

sleep(10)

city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) # swap with your location

while True:

    s = sun(city.observer)
    time_now = utc.localize(datetime.now())
    sunrise = s["sunrise"]
    dusk = s["dusk"]

    if time_now > sunrise and time_now < dusk: # am I in between sunrise and dusk?
        if pifacedigital.output_pins[3].value == 1: # if the relay is not off
            pifacedigital.output_pins[3].turn_off() # switch it off
    else:
        if pifacedigital.output_pins[3].value != 1: # if the relay is not on
            pifacedigital.output_pins[3].turn_on() # switch it on

    sleep(60)

In the if-else, the system checks whether the light is already switched on or not. With this I prevent a short flickering because otherwise the piFace Digital will switch the light.

I also convert time_now to UTC because otherwise I cannot compare. The date including time from Astral comes with a time zone specification and if I do not convert time_now, the program tells me: TypeError: can’t compare offset-naive and offset-aware datetimes

This is basically: you cannot compare apples with oranges.

My Pi is running in the timezone UTC, just to make things easier. If you want to work with a time zone, you could for example use timedelta to adjust your current time by x hours to match your timezone. There are several possibilities at this point. Here an example with plus and minus 2 hours.

import datetime

tdplus = datetime.datetime.now() + datetime.timedelta(hours=2)
tdminus = datetime.datetime.now() - datetime.timedelta(hours=2)

print(tdplus)
print(tdminus)

Switching GPIO Pins with Astral

The script can of course also be easily customized to switch GPIO pins. As said before, you can switch a suitable relay with it or you just experiment with an LED.

from astral import LocationInfo
from astral.sun import sun
from datetime import datetime
from time import sleep
from gpiozero import LED
import pytz

utc=pytz.UTC

led = LED(17)

city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) # swap with your location


while True:

    s = sun(city.observer)
    time_now = utc.localize(datetime.now())
    sunrise = s["sunrise"]
    dusk = s["dusk"]

    if time_now > sunrise and time_now < dusk: # am I in between sunrise and dusk?
        led.off()
    else:
        led.on()

    sleep(60)

In my case, I switch GPIO17 (pin 11).

Maybe I will change the behaviour to the end of the dawn. The advantage of this solution compared to a conventional timer is that I can really only switch the night light during the dark hours. Another advantage is that it is simply fun to fiddle with. 🙂

Astral can do a lot more

While we are working with Astral let’s have a look at a few practical examples.

In the documentation you can read that location data for various cities are available in a database. All capitals are included, plus a few additional cities from Great Britain and the USA. You can query the database as follows:

from astral import LocationInfo
from astral.geocoder import database, lookup

city = lookup("Amsterdam", database())
print(city)

This gives me the following output:

The database of Astral contains location information from all capitals
The database of Astral contains location information from all capitals

I can also query the values individually:

from astral import LocationInfo
from astral.geocoder import database, lookup

city = lookup("Berlin", database())

print(city.name)
print(city.region)
print(city.timezone)
print(city.latitude)
print(city.longitude)

But I don’t have to use the database, I can define my location. Of course, I don’t have to import astral.geocoder for this. If you want the calculation to be as exact as possible, it is best to define latitude and longitude yourself:

from astral import LocationInfo

city = LocationInfo(name="Cordoba", region="Spain", timezone="Europe/Spain", latitude=37.8833333, longitude=-4.7666667)

print((
    f"Information for {city.name}/{city.region}\n"
    f"Timezone: {city.timezone}\n"
    f"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\n" ))

OK, now we’ve defined our location. Let’s see what we can do with it.

Sunrise, sunset, dawn and dusk

With the location, Astral calculates the desired times, by default in UTC. This is relatively simple and looks like this:

from astral import LocationInfo
from astral.sun import sun

city = LocationInfo(name="Cordoba", region="Spain", timezone="Europe/Spain", latitude=37.8833333, longitude=-4.7666667)

c_data = sun(city.observer)

print("Sunrise: " + str(c_data["sunrise"]))
print("Sunset: " + str(c_data["sunset"]))
print("Dawn: " + str(c_data["dawn"]))
print("Dusk: " + str(c_data["dusk"]))

It looks like that:

With Astral I get sunrise, sunset, dawn and dusk – easy
With Astral I get sunrise, sunset, dawn and dusk — easy

Note: The times are calculated when the sun breaks through the horizon in clear visibility — obstacles in the eye of the observer are ignored.

If you do not enter a date, Astral will calculate with the current date. You can also have the values calculated for a specific date. You only have to specify it when defining c_data and import datetime. Here is an example for October 21, 2019:

from astral import LocationInfo
from astral.sun import sun

city = LocationInfo(name="Cordoba", region="Spain", timezone="Europe/Spain", latitude=37.8833333, longitude=-4.7666667)

c_data = sun(city.observer, date=datetime.date(2019, 10, 21))

print("Sunrise: " + str(c_data["sunrise"]))
print("Sunset: " + str(c_data["sunset"]))
print("Dawn: " + str(c_data["dawn"]))
print("Dusk: " + str(c_data["dusk"]))

The documentation says that you can also display information about the golden hour and blue hour. Especially for photographers this is important information.

Golden hour and blue hour

The blue hour and the golden hour appear twice a day. In simple terms: blue hour > the sun is below the horizon and golden hour > just above. In the blue hour the blue light spectrum dominates and in the golden hour the colours are much softer and warmer.

Golden hour
Golden hour

Unlike sunrise or sunset, blue hour and golden hour have two values: beginning and end. If you do not give an additional value, Astral calculates to the rising sun by default. Let’s get the data:

from astral import LocationInfo
from astral.sun import golden_hour
from astral.sun import blue_hour

city = LocationInfo(name="Cordoba", region="Spain", timezone="Europe/Spain", latitude=37.8833333, longitude=-4.7666667)

gh = golden_hour(city.observer)
bh = blue_hour(city.observer)

print("Golden hour start: " + str(gh[0]))
print("Golden hour end: " + str(gh[1]))
print("Blue hour start: " + str(bh[0]))
print("Blue hour end: " + str(bh[1]))

If I want to calculate the golden hour and the blue hour for the evening, I have to set the direction of the sun to SETTING. RISING is also available, but that is the default, as already mentioned. If I want to get the data for the evening this, the code looks like this (golden hour in the evening and blue hour in the morning):

from astral import LocationInfo
from astral.sun import SunDirection
from astral.sun import golden_hour
from astral.sun import blue_hour
from datetime import datetime

city = LocationInfo(name="Cordoba", region="Spain", timezone="Europe/Spain", latitude=37.8833333, longitude=-4.7666667)

gh = golden_hour(city.observer, datetime.now().date(), SunDirection.SETTING)
bh = blue_hour(city.observer)

print("Golden hour start: " + str(gh[0]))
print("Golden hour end: " + str(gh[1]))
print("Blue hour start: " + str(bh[0]))
print("Blue hour end: " + str(bh[1]))

You can also see that the golden hour and the blue hour are not really 60 minutes. It depends a lot on location and season. So, it’s good to know when to have your camera ready.

Blue hour
Blue hour

By the way, we can also use Astral to get information about the moon.

Calculate moon phases with Astral

The tool can also output the moon phases. Using the date, the Python program spits out a number between 0 and 28. In the documentation we find how to interpret the numbers of the moon phases:

  • 0 – 6.99 — new moon
  • 7 – 7.99 — waxing moon
  • 14 – 20.99 — full moon
  • 21 – 27.99 — waning moon

To make things a bit more exciting, let’s have the moon phase of yesterday, today and tomorrow displayed. Especially the following day makes sense, if you want to know when the moon is at its fullest, for example:

from astral.moon import phase
import datetime

moon_today = phase()
moon_yesterday = phase(datetime.datetime.now() - datetime.timedelta(days=1))
moon_tomorrow = phase(datetime.datetime.now() + datetime.timedelta(days=1))

print("Mond gestern", end=": ")
print(moon_yesterday)
print("Mond heute", end=": ")
print(moon_today)
print("Mond morgen", end=": ")
print(moon_tomorrow)

If you are into star photography you want to have as little moon as possible, right? With the following script we find out, for example, when the moon is the fullest and least visible in the next 365 days:

from astral.moon import phase
import datetime

moon_dic = {}

for var in list(range(365)):
    date_temp = datetime.datetime.now().date() + datetime.timedelta(days=(var))
    moon_phase = phase(date_temp)
    moon_dic[moon_phase] = date_temp

darkest = min(moon_dic)
print(moon_dic.get(darkest), end=": ")
print(darkest)

darkest2 = max(moon_dic)
print(moon_dic.get(darkest2), end=": ")
print(darkest2)

Now I know that during the next 365 days the moon will be very little visible on September 17, 2020 and May 11, 2021.

When it’s at its fullest, I can’t say exactly. The tool tells me that between 14 – 20.99 is full moon — but not exactly when the moon is fullest. Is that exactly halfway, so 14? I’ll stare at the sky and try to find that out for myself at the next full moon.

Get the moon phases with Astral
Get the moon phases with Astral

The tool can calculate some other data. At the moment that is all I need. I can get what I want and have the data sent to my smartphone (with pushover like my Coronavirus data).

Now I have my Bitcoin Ticker, a Coronavirus Ticker and a tool for sunrise, sunset, blue hour, golden hour and moon phases. I know that there are different apps for this, but I need to install less stuff on my smartphone and I can also customize the tools the way I want to. This way I can have very specific information sent to me.

I hope that was clear

I wrote this article to understand Astral a little better myself. A few things are not obvious to me yet, but on the whole I understand how to use Astral.

Maybe this article will help you to do something useful with the tool. Now that I understand how to use it, I really like it. In any case, there are many possibilities how to use the data calculated by Astral.

Leave a Reply

Your email address will not be published. Required fields are marked *