Monday, June 8, 2020

Accessing Time and Setting Alarms in Python on the Raspberry Pi


Recently I started coding in Python. Sometimes it was easier and sometimes it was harder for me to find help on the web. Therefore, I decided to document the pieces that I figured out, for myself as a future reference, and also for others.
The following tutorial is about how to access the date and time information in Python on a Raspberry Pi (but it is not restricted to the RPi) and how this can be applied to build an alarm clock or to add such a feature to an existing RPi set-up. Personally, I'm using Python3, but the code below should also work in Python version 2.

Prerequisite: Playing Audio with "aplay"

The example below for the alarm clock is playing a prerecorded audio clip as the alarm signal. For this purpose, it requires a sound file "alarm.wav" (in .wav format!) to be stored in the home directory (/home/pi/alarm.wav). And it assumes that the audio output is set to the 3.5mm headphone jack (using sudo raspi-config -> audio settings). So, before proceeding with the alarm clock example below, make sure you are able to play audio using the following example. Save the following code in a file "audio-test.py", and start it from the command line as "python3 ./audio-test.py".

import subprocess
# -----------------------------------------------------------
# --- set the audio volume -> here for headphone output
# -----------------------------------------------------------
volume = subprocess.Popen(["amixer", "set", "Headphone,0", "100%"])
clip = '/home/pi/alarm.wav'
alarm = subprocess.Popen(["aplay", clip]) # ----------------------------------------------------------- # --- end of code # -----------------------------------------------------------

Accessing Time Information

If the above code did not work, maybe get back to that later. To follow the alarm clock example, just remove (or comment "#") the lines with the "aplay" commands, and use the printouts.
While there are many Python modules out there that do fancy things, I wanted to keep this as minimal as possible, and only use standard Python module "time". A simple application of the time module functions is to make a program wait for a given amount of seconds, like
time.sleep(1.5)
or
time.sleep(0.1)
Please note that the argument does not have to be an integer, so this can be used to wait for any fraction of a second.

The time module is accessing the current date and time information via the system function "localtime()" of the RPi. Since the RPi does not have a clock, this requires a internet connection. The current date/time can be checked from the command line by typing "date <Return>" for which the output looks like
Sun Jun  7 23:26:17 CDT 2020
Compare this with your current time to make sure that the timezone is set correctly. If not, use "sudo raspi-config" and adjust the settings in "Change Timezone".

To access the date and time information, the time module provides the function "time.strftime()". This function (which returns a character string) requires an argument that specifies the formatting. In my code, I prefer to work with numbers instead of strings. So I am not looking into the options, how to nicely format the output of this command. I am only interested in the simplest possible way how to extract the relevant information and convert it into integer numbers.

The command:
hour = time.strftime(%H)
extracts the current hour (in 24-hour format) as a string (00-23) and save the result in the string variable "hour". This string can be converted and stored in an integer variable using
h = int(hour)
Other quantities are extracted correspondingly, using the following arguments in "time.strftime()":
%H        hour (24-hour format) 00-23
%I        hour (12-hour format) 01-12 %M        minute 00-59 %S        second 00-59 %d        day of month as decimal number 01-31 %m        month as decimal number 01-12 %y        year without century 00-99 %Y        year with century (eg: 2020) %w        weekday as decimal number (Sun:0, Mon:1, ..., Sat:6) %U        week number of the year 00-53 (days before the first Sunday are in week 0) 
Other possible arguments return strings for the month and day, but as I stated, I'm only interested in numbers.

Code for an Alarm Clock

With all of the above we can understand the code for the alarm clock. Copy and paste the following code into "alarm.py", and run it with "python3 ./alarm.py" (or "python ./alarm.py"). I added plenty of comments, so (with the discussion above) the code should be clear and easy to read.
Remember to store a .wav file called ("alarm.wav") in the home directory of the RPi (or modify the definition of the variable "clip" if you use a file with a different name and/or location). 
Where it states "# here: insert ... task", you can call other Python code to perform any possible tasks that you like.

import time
import subprocess
print(" ")
print("  ============================================")
print("       ---> demo code for time access")
print("                and coding alarms")
print(" ")


# --- store the audio file to be played when the alarm starts
clip = '/home/pi/alarm.wav'
# --- example: wait for 1.5 sec time.sleep(1.5)
# --- example: wait for 0.1 sec
time.sleep(0.1)
# -----------------------------------------------------------
# --- set the audio volume -> here for headphone output
# -----------------------------------------------------------
volume = subprocess.Popen(["amixer", "set", "Headphone,0", "100%"])
# -----------------------------------------------------------
# --- get the initial time
# -----------------------------------------------------------
#
# --- time.strftime() gets information from system via localtime()
# --- get weekday, hour (in 24h format), min as strings
weekday = time.strftime("%w") 
hour = time.strftime("%H") 
minute = time.strftime("%M")  # # --- convert all into integer variables lasthour = int(hour) lastminute = int(minute)

print(" current time  ",hour,":",minute,sep='')
print(" day:",weekday,"  (0-6 for Sun-Sat)")
print("")
# -------------------------------------------------------------
# ---              infinite loop
# --- continuously read the time - and start alarm when appropriate # -------------------------------------------------------------
while True:

  # - get day/hour/minute as strings
  weekday = time.strftime("%w") 
  hour = time.strftime("%H") 
  minute = time.strftime("%M") 
  # - convert the strings to integer variables
  w = int(weekday)          
  h = int(hour)
  m = int(minute) # ------ now comes a list of examples for alarms

  # - for hourly alarms
  if (h != lasthour):
    print("  >>> a new hour has begun:",hour)
    alarm = subprocess.Popen(["aplay", clip]) # here: insert hourly task 
    
  # - every half-hour alarm
  if (m != lastminute and m == 30):
    print("  >>> new half-hour:",hour,":",minute)
    alarm = subprocess.Popen(["aplay", clip]) # here: insert half-hourly task 

  # - for alarms every 10 minutes
  if (m != lastminute and (m%10) == 0):
    print("  >>> ten minutes have passed:",minute) alarm = subprocess.Popen(["aplay", clip])
    # here: insert task to be done every 10 minutes 
  # - for alarms every minute
  if (m != lastminute):
    print("  >>> a new minute begun:",minute) alarm = subprocess.Popen(["aplay", clip])
    # here: insert task to be done every minute

  # - for alarms at 8:15
  if (m != lastminute and h == 8 and m == 15): 
    print("  >>> alarm at:",hour,":",minute) alarm = subprocess.Popen(["aplay", clip])
    # here: insert task to be done

  # - alarm Mon-Fri at 9:30
  if (m != lastminute and w > 0 and w < 6 and h == 9 and m == 30): 
    print("  >>> alarm (only Mon-Fri) at:",hour,":",minute) alarm = subprocess.Popen(["aplay", clip])
    # here: insert task to be done
  # - alarm on Mon, Wed, Fri at 14:45 (= 2:45pm) 
  if (m != lastminute and (w%2) == 1 and h == 14 and m == 45): 
    print("  >>> Mon,Wed,Fri alarm at:",hour,":",minute) alarm = subprocess.Popen(["aplay", clip])
    # here: insert task to be done
  # - remember the last values of hour and minute, so the above alarms
  #   are only fired once (and not over and over again)
  lasthour = h
  lastminute = m
  time.sleep(0.2)
# ---------------------- end of code ---------------------------
# --------------------------------------------------------------
With the comments, the code should (hopefully!) be pretty clear. There are only two details that may require an explanation, the statements (m%10)  and (w%2).

The Modulo Operator

The "%" sign stands for the "modulo" operator which returns the rest in an integer division. Examples:
  • 15 / 3 = 5 and there is zero rest. So: 15%5 = 0 
  • 17 / 3 = 5 with a rest of two. So: 17%5 = 2
  • 40/10 = 4 with a rest of zero. So: 40%10 = 0
  • 43/10 = 4 with a rest of three. So: 43%10 = 3
  • 3/10 = 0 with a rest of three. So: 3%10 = 3
Therefore, the expression (m%10) is equal to zero, when the minutes "m" are equal to 0, 10, 20, 30, 40, 50. So the corresponding alarm fires every ten minutes.
The expression (w%2) is one, whenever, the weekday "w" is equal to 1, 3, 5 - and these correspond to Monday, Wednesday, Friday.

Please let me know if this was helpful - or if not.


No comments: