This brings up to the openweather_graphics.py file. This code remains largely unchanged from its original state from the original PyPortal Weather display project. There has just been some minor edits to the layout and some of the data that is being pulled.
First off, the purpose of openweather_graphics.py is to take care of all the display setup for the weather data, aka the default display for this project. If you had no alarms setup, then this is the only thing that you would see. It's written a lot like a Python library, since it contains a class
with functions inside of it that can then be called elsewhere; in this case in code.py. With this setup, it means that a lot of complex graphical things can be called upon with just a couple of lines of code in code.py, keeping everything nice and neat.
First, just like with other CircuitPython code you're familiar with, libraries are imported. You'll notice that the calendar.py file is being imported, specifically the holidays
portion, in order to reference them later.
import time import json from calendar import holidays import displayio from adafruit_display_text.label import Label from adafruit_bitmap_font import bitmap_font
This is followed by some setup to allow for our fonts to be imported. We're using four different bitmap fonts. The font family is the same for all of them but the size varies. They all live in the fonts folder, which you'll be able to download in the zipped file from GitHub.
cwd = ("/"+__file__).rsplit('/', 1)[0] # the current working directory (where this file is) small_font = cwd+"/fonts/EffectsEighty-24.bdf" medium_font = cwd+"/fonts/EffectsEighty-32.bdf" weather_font = cwd+"/fonts/EffectsEighty-48.bdf" large_font = cwd+"/fonts/EffectsEightyBold-68.bdf"
Then we have some arrays that we'll be referencing later in the functions. Basically the month_name
and weekday
arrays allow for string formatting for the month and days of the week to display on the PyPortal. holiday_checks
allows us to bring in the dates of the holidays setup in the calendar.py file and the holiday_greetings
are strings that are setup in the calendar.py file and will display on the screen if openweather_graphics.py detects that it is indeed a holiday. We'll go over the function that allows for this to happen, update_date()
, in a bit.
month_name = ["Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."] weekday = ["Mon.", "Tues.", "Wed.", "Thurs.", "Fri.", "Sat.", "Sun."] holiday_checks = [holidays['new years'][0],holidays['valentines'][0], holidays['halloween'][0],holidays['xmas'][0]] holiday_greetings = [holidays['new years'][1],holidays['valentines'][1], holidays['halloween'][1],holidays['xmas'][1]]
This brings us to our class
definition. The __init__
section basically sets up all of the display components that will be used for our weather graphic. The bitmap fonts are loaded in and the text groups are created for each component along with their location on the screen and color. The default bitmap is also defined here as being weather_background.bmp. That's the fun Retro Weather graphic you see when you boot-up the PyPortal with this code.
The weather_background.bmp file.
class OpenWeather_Graphics(displayio.Group): def __init__(self, root_group, *, am_pm=True, celsius=True): super().__init__() self.am_pm = am_pm self.celsius = celsius root_group.append(self) self._icon_group = displayio.Group() self.append(self._icon_group) self._text_group = displayio.Group() self.append(self._text_group) self._icon_sprite = None self._icon_file = None self.set_icon(cwd+"/weather_background.bmp") self.small_font = bitmap_font.load_font(small_font) self.medium_font = bitmap_font.load_font(medium_font) self.weather_font = bitmap_font.load_font(weather_font) self.large_font = bitmap_font.load_font(large_font) glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: ' self.small_font.load_glyphs(glyphs) self.medium_font.load_glyphs(glyphs) self.weather_font.load_glyphs(glyphs) self.large_font.load_glyphs(glyphs) self.large_font.load_glyphs(('°',)) # a non-ascii character we need for sure self.city_text = None self.holiday_text = None self.time_text = Label(self.medium_font) self.time_text.x = 365 self.time_text.y = 15 self.time_text.color = 0x5AF78E self._text_group.append(self.time_text) self.date_text = Label(self.medium_font) self.date_text.x = 10 self.date_text.y = 15 self.date_text.color = 0x57C6FE self._text_group.append(self.date_text) self.temp_text = Label(self.large_font) self.temp_text.x = 316 self.temp_text.y = 165 self.temp_text.color = 0xFF6AC1 self._text_group.append(self.temp_text) self.main_text = Label(self.weather_font) self.main_text.x = 10 self.main_text.y = 258 self.main_text.color = 0x99ECFD self._text_group.append(self.main_text) self.description_text = Label(self.small_font) self.description_text.x = 10 self.description_text.y = 296 self.description_text.color = 0x9FA0A2 self._text_group.append(self.description_text)
This brings us to our first function, display_weather()
. It basically takes the JSON data that the PyPortal will parse (this is taken care of with code.py) and puts the different bits into the different graphical parameters that we just defined in __init__
. For example, the weather description in the JSON data from OpenWeatherMaps is loaded as a string into the main_text
object. display_weather()
will be called in code.py to update the weather data displayed on screen.
Two other important things are happening here related to JSON data. The city.name
variable will take our location parameter from code.py, which will have been imported from secrets.py. The location will be printed as a string onto our weather graphic, but it's also related to the JSON data being parsed in general, since OpenWeatherMap is location dependent.
Second, you'll notice a self.set_icon
function (which is setup last in this file). This controls what 8-bit weather icon is displayed depending on the weather. These images are stored on an SD card that you'll insert into the PyPortal inside a folder called icons. Why an SD card? Because of the higher resolution of the PyPortal Titano and the number of icons, there simply isn't enough room for them to live onboard along with all of the other graphics, fonts, sounds and code files needed for this project.
def display_weather(self, weather): weather = json.loads(weather) # set the icon/background weather_icon = weather['weather'][0]['icon'] self.set_icon("/sd/icons/"+weather_icon+".bmp") city_name = weather['name'] + ", " + weather['sys']['country'] print(city_name) if not self.city_text: self.city_text = Label(self.medium_font, text=city_name) self.city_text.x = 300 self.city_text.y = 296 self.city_text.color = 0xCF5349 self._text_group.append(self.city_text) self.update_time() main_text = weather['weather'][0]['main'] print(main_text) self.main_text.text = main_text temperature = weather['main']['temp'] - 273.15 # its...in kelvin print(temperature) if self.celsius: self.temp_text.text = "%d °C" % temperature else: self.temp_text.text = "%d °F" % ((temperature * 9 / 5) + 32) description = weather['weather'][0]['description'] description = description[0].upper() + description[1:] print(description) self.description_text.text = description # "thunderstorm with heavy drizzle"
Next are two very similar functions: update_time()
and update_date()
. update_time()
essentially updates the clock displayed on the weather graphic. It will be called in code.py so that the clock stays current on the display. This data is being pulled down using time.localtime()
, a Python time function.
def update_time(self): """Fetch the time.localtime(), parse it out and update the display text""" now = time.localtime() hour = now[3] minute = now[4] format_str = "%d:%02d" if self.am_pm: if hour >= 12: hour -= 12 format_str = format_str+" PM" else: format_str = format_str+" AM" if hour == 0: hour = 12 time_str = format_str % (hour, minute) print(time_str) self.time_text.text = time_str
update_date()
is similar to update_time()
, with the only difference being that instead of the clock, it is updating the date displayed on the weather display. It will also be called in code.py. It's pulling in our month_name
and weekday
arrays to format the strings that will be displayed. Additionally though, this function is checking to see if the current date being pulled in with time.localtime()
matches with any of the dates that we defined in calendar.py for the holidays that we setup. It checks based on the holiday_checks
array and then if it is a holiday, the string from calendar.py pulled in with the holiday_greetings
array will be displayed right below the date on the weather graphic. We'll go into how these checks are working in a little more detail for code.py, because the same principle concept is being used to check for alarms.
def update_date(self): date_now = time.localtime() year = date_now[0] mon = date_now[1] date = date_now[2] day = date_now[6] today = weekday[day] month = month_name[mon - 1] date_format_str = " %d, %d" shortened_date_format_str = " %d" date_str = today+", "+month+date_format_str % (date, year) holiday_date_str = month+shortened_date_format_str % (date) print(date_str) self.date_text.text = date_str for i in holiday_checks: h = holiday_checks.index(i) if holiday_date_str == holiday_checks[h]: if not self.holiday_text: self.holiday_text = Label(self.medium_font, max_glyphs=60) self.holiday_text.x = 10 self.holiday_text.y = 45 self.holiday_text.color = 0xf2f89d self._text_group.append(self.holiday_text) self.holiday_text.text = holiday_greetings[h]
Finally, the last function, which we saw called in the display_weather()
function, is setup. set_icon()
basically allows for our changing weather icon bitmaps to be added to the graphics group that is running for the weather graphics display. You'll see some familiar OnDiskBitmap()
setup happening if you have experience with the displayio
library.
def set_icon(self, filename): """The background image to a bitmap file. :param filename: The filename of the chosen icon """ print("Set icon to ", filename) if self._icon_group: self._icon_group.pop() if not filename: return # we're done, no icon desired if self._icon_file: self._icon_file.close() # CircuitPython 6 & 7 compatible self._icon_file = open(filename, "rb") icon = displayio.OnDiskBitmap(self._icon_file) self._icon_sprite = displayio.TileGrid(icon, pixel_shader=getattr(icon, 'pixel_shader', displayio.ColorConverter())) # # CircuitPython 7+ compatible # icon = displayio.OnDiskBitmap(filename) # self._icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader) self._icon_group.append(self._icon_sprite)
Page last edited March 08, 2024
Text editor powered by tinymce.