From e17b69c81837f4c7158fe00aeac3532d3ef19056 Mon Sep 17 00:00:00 2001 From: Tristan Date: Thu, 23 Mar 2023 07:02:02 -0400 Subject: [PATCH] Weather Display Added current weather icon, current temperature, temperature trend, daily min/max temperature from Openweather to display; added time data from Adafruit IO --- bdf/trend_icons.bdf | 54 +++++++++++++++ bmp/weather-icons.bmp | Bin 0 -> 9272 bytes code.py | 121 ++++++++++++++++++++++++++++++++ display_manager.py | 156 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 bdf/trend_icons.bdf create mode 100644 bmp/weather-icons.bmp diff --git a/bdf/trend_icons.bdf b/bdf/trend_icons.bdf new file mode 100644 index 0000000..b8dd3e4 --- /dev/null +++ b/bdf/trend_icons.bdf @@ -0,0 +1,54 @@ +STARTFONT 2.1 +FONT -Trend Icon Set +SIZE 20 75 75 +FONTBOUNDINGBOX 39 31 -11 -6 +STARTPROPERTIES 40 +FONT_ASCENT 17 +FONT_DESCENT 4 +UNDERLINE_POSITION -1 +UNDERLINE_THICKNESS 2 +X_HEIGHT 10 +CAP_HEIGHT 14 +DEFAULT_CHAR 1 +RAW_ASCENT 800 +RAW_DESCENT 200 +NORM_SPACE 6 +RELATIVE_WEIGHT 70 +RELATIVE_SETWIDTH 50 +SUPERSCRIPT_X 0 +SUPERSCRIPT_Y 7 +SUPERSCRIPT_SIZE 12 +SUBSCRIPT_X 0 +SUBSCRIPT_Y 1 +SUBSCRIPT_SIZE 12 +AVG_LOWERCASE_WIDTH 109 +AVG_UPPERCASE_WIDTH 142 +ENDPROPERTIES +CHARS 1300 +STARTCHAR NULL +ENCODING 0 +SWIDTH 0 0 +DWIDTH 0 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR comma +ENCODING 44 +SWIDTH 333 0 +DWIDTH 5 0 +BBX 3 4 1 -2 +BITMAP +40 +A0 +ENDCHAR +STARTCHAR period +ENCODING 46 +SWIDTH 333 0 +DWIDTH 5 0 +BBX 3 4 1 -2 +BITMAP +A0 +40 +ENDCHAR +ENDFONT \ No newline at end of file diff --git a/bmp/weather-icons.bmp b/bmp/weather-icons.bmp new file mode 100644 index 0000000000000000000000000000000000000000..95be1fec6ae79ae3ae557dfc63f3bd13c53060fc GIT binary patch literal 9272 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 F{s*icsiXh^ literal 0 HcmV?d00001 diff --git a/code.py b/code.py index f90b869..b60f70d 100644 --- a/code.py +++ b/code.py @@ -4,12 +4,16 @@ import busio from digitalio import DigitalInOut, Pull import neopixel + from adafruit_matrixportal.matrix import Matrix from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager + import json import display_manager +print("All imports loaded | Available memory: {} bytes".format(gc.mem_free())) + # --- CONSTANTS SETUP --- try: @@ -22,6 +26,17 @@ station_code = secrets["station_code"] historical_trains = [None, None] +# weather data dict +weather_data = {} +# daily highest temperature +# max_temp, day of the year +highest_temp = [None,None] +# daily lowest temperature +# min_temp, day of the year +lowest_temp = [None, None] +# current temp (for historical) +current_temp = [] + # --- DISPLAY SETUP --- # MATRIX DISPLAY MANAGER @@ -110,11 +125,117 @@ def get_trains(StationCode, historical_trains): pass return trains +# queries Openweather API to return a dict with current and 3 hr forecast weather data +# input is latitude and longitude coordinates for weather location +def get_weather(weather_data): + try: + # query Openweather for weather at location defined by input lat, long + base_URL = 'https://api.openweathermap.org/data/3.0/onecall?' + latitude = secrets['dc coords x'] + longitude = secrets['dc coords y'] + units = 'imperial' + api_key = secrets['openweather api key'] + exclude = 'minutely,alerts' + response = wifi.get(base_URL + +'lat='+latitude + +'&lon='+longitude + +'&exclude='+exclude + +'&units='+units + +'&appid='+api_key + ) + weather_json = response.json() + del response + + # insert icon and current weather data into dict + weather_data["icon"] = weather_json["current"]["weather"][0]["icon"] + weather_data["current_temp"] = weather_json["current"]["temp"] + weather_data["current_feels_like"] = weather_json["current"]["feels_like"] + # insert daily forecast min and max temperature into dict + weather_data["daily_temp_min"] = weather_json["daily"][0]["temp"]["min"] + weather_data["daily_temp_max"] = weather_json["daily"][0]["temp"]["max"] + # insert next hour + 1 forecast temperature and feels like into dict + weather_data["hourly_next_temp"] = weather_json["hourly"][2]["temp"] + weather_data["hourly_feels_like"] = weather_json["hourly"][2]["feels_like"] + + # set daily highest temperature + global highest_temp + global current_time + # if daily highest temperature hasn't been set or is from a previous day + if highest_temp[0] is None or highest_temp[1] != current_time.tm_wday: + highest_temp[0] = weather_data["daily_temp_max"] + highest_temp[1] = current_time.tm_wday + print("Daily highest temp set to {}".format(highest_temp[0])) + # if stored highest temp is less than new highest temp + elif highest_temp[0] < weather_data["daily_temp_max"]: + highest_temp[0] = weather_data["daily_temp_max"] + print("Daily highest temp set to {}".format(highest_temp[0])) + # if stored highest temp is greater than new highest temp + elif highest_temp[0] > weather_data["daily_temp_max"]: + weather_data["daily_temp_max"] = highest_temp[0] + print("Daily highest temp pulled from historical data") + + # set daily lowest temperature + global lowest_temp + # if daily lowest temperature hasn't been set or is from a previous day + if lowest_temp[0] is None or lowest_temp[1] != current_time.tm_wday: + lowest_temp[0] = weather_data["daily_temp_min"] + lowest_temp[1] = current_time.tm_wday + print("Daily lowest temp set to {}".format(lowest_temp[0])) + # if daily lowest temp is greater than new lowest temp + elif lowest_temp[0] > weather_data["daily_temp_min"]: + lowest_temp[0] = weather_data["daily_temp_min"] + print("Daily lowest temp set to {}".format(lowest_temp[0])) + # if daily lowest temp is less than new lowest temp + elif lowest_temp[0] < weather_data["daily_temp_min"]: + weather_data["daily_temp_min"] = lowest_temp[0] + print("Daily lowest temp pulled from historical data") + + # add current temp to historical array + global current_temp + current_temp.append(weather_data["current_temp"]) + # clean up response + del weather_json + + # return dict with relevant data + return True + + except Exception as e: + print("Failed to get data, retrying\n", e) + wifi.reset() + +# --- TIME MGMT FUNCTIONS --- + +def check_time(): + base_url = "http://io.adafruit.com/api/v2/time/seconds" + try: + response = wifi.get(base_url) + epoch_time = int(response.text) + del response + + global current_time + current_time = time.localtime(epoch_time) + except Exception as e: + print(e) + wifi.reset() + # --- OPERATING LOOP ------------------------------------------ loop_counter=1 +last_weather_check=None last_train_check=None while True: + check_time() + + # fetch weather data on start and recurring (default: 10 minutes) + if last_weather_check is None or time.monotonic() > last_weather_check + 60 * 10: + weather = get_weather(weather_data) + if weather: + last_weather_check = time.monotonic() + print("weather updated") + # update weather display component + display_manager.update_weather(weather_data) + else:pass + # update train data (default: 15 seconds) if last_train_check is None or time.monotonic() > last_train_check + 15: trains = get_trains(station_code, historical_trains) diff --git a/display_manager.py b/display_manager.py index 626b359..3cceca5 100644 --- a/display_manager.py +++ b/display_manager.py @@ -1,4 +1,6 @@ # Display Manager +# Structure and some code from Weather Display Matrix project: +#https://learn.adafruit.com/weather-display-matrix/code-the-weather-display-matrix import time import displayio @@ -6,6 +8,15 @@ from adafruit_display_text.label import Label from adafruit_bitmap_font import bitmap_font +cwd = ("/" + __file__).rsplit("/", 1)[0] + +icon_spritesheet = cwd + "/bmp/weather-icons.bmp" +icon_width = 16 +icon_height = 16 + +# custom font for temperature trend indicators +symbol_font = bitmap_font.load_font("/bdf/trend_icons.bdf") + # custom colors hex codes metro_orange=0xf06a37 metro_red=0xda1b30 @@ -22,30 +33,92 @@ def __init__( self.root_group = displayio.Group() self.root_group.append(self) + # create parent weather group for weather display groups + self._weather_group = displayio.Group() + self._weather_group.hidden = False + self.append(self._weather_group) + + # create current weather icon group + self._icon_group = displayio.Group(x=4, y=2) + self._weather_group.append(self._icon_group) + + # create current temperature group + self._current_temp_group = displayio.Group() + self._weather_group.append(self._current_temp_group) + + # create temperature trend symbol group + self._temp_trend_group = displayio.Group() + self._weather_group.append(self._temp_trend_group) + + # create min max temperature group + self._min_max_temp_group = displayio.Group() + self._weather_group.append(self._min_max_temp_group) + # create parent train group for train Labels self._train_board_group = displayio.Group() self._train_board_group.hidden = False self.append(self._train_board_group) # set default column and row measurements - self.col1=16 - self.col2=96 + self.col1=4 + self.col2=28 + self.col25=52 + self.col3=108 self.row1=8 self.row2=24 + # Load the icon sprite sheet + icons = displayio.OnDiskBitmap(open(icon_spritesheet, "rb")) + self._icon_sprite = displayio.TileGrid( + icons, + pixel_shader=getattr(icons, 'pixel_shader', displayio.ColorConverter()), + tile_width=icon_width, + tile_height=icon_height + ) + + # set current temperature text + # left-middle column, top row + self.temp_text = Label(terminalio.FONT) + self.temp_text.x = self.col2 - 4 + self.temp_text.y = self.row1 + self.temp_text.color = 0xFFFFFF + self._current_temp_group.append(self.temp_text) + + # set current temperature trend icon + self.temp_trend_icon = Label(font=symbol_font) + self.temp_trend_icon.x = self.col2 + 8 + self.temp_trend_icon.y = self.row1 - 7 + self._temp_trend_group.append(self.temp_trend_icon) + + # set daily minimum temperature text + # left column, bottom row + self.min_temp_text = Label(terminalio.FONT) + self.min_temp_text.x = self.col1 + 2 + self.min_temp_text.y = self.row2 + self.min_temp_text.color = 0x1e81b0 + self._min_max_temp_group.append(self.min_temp_text) + + # set daily maximum temperature text + # left-middle column, bottom row + self.max_temp_text = Label(terminalio.FONT) + self.max_temp_text.x = self.col2 - 4 + self.max_temp_text.y = self.row2 + self.max_temp_text.color = metro_red + self._min_max_temp_group.append(self.max_temp_text) + # set top row of train destination text # right-middle column, top row self.top_row_train_text = Label(terminalio.FONT) - self.top_row_train_text.x = self.col1 + self.top_row_train_text.x = self.col25 self.top_row_train_text.y = self.row1 self.top_row_train_text.color = metro_orange - self.top_row_train_text.text = "Shady Grove" + self.top_row_train_text.text = "Shady Gr" self._train_board_group.append(self.top_row_train_text) # set top row of train time to arrival text # right column, top row self.top_row_train_min = Label(terminalio.FONT) - self.top_row_train_min.x = self.col2 + self.top_row_train_min.x = self.col3 self.top_row_train_min.y = self.row1 self.top_row_train_min.color = metro_orange self.top_row_train_min.text = "0" @@ -54,7 +127,7 @@ def __init__( # set bottom row of train destination text # right-middle column, bottom row self.bottom_row_train_text = Label(terminalio.FONT) - self.bottom_row_train_text.x = self.col1 + self.bottom_row_train_text.x = self.col25 self.bottom_row_train_text.y = self.row2 self.bottom_row_train_text.color = metro_orange self.bottom_row_train_text.text = "Glenmont" @@ -63,12 +136,37 @@ def __init__( # set bottom row of train time to arrival text # right column, bottom row self.bottom_row_train_min = Label(terminalio.FONT) - self.bottom_row_train_min.x = self.col2 + self.bottom_row_train_min.x = self.col3 self.bottom_row_train_min.y = self.row2 self.bottom_row_train_min.color = metro_orange self.bottom_row_train_min.text = "0" self._train_board_group.append(self.bottom_row_train_min) + # default icon set to none + self.set_icon(None) + + def set_icon(self, icon_name): + """Use icon_name to get the position of the sprite and update + the current icon. + :param icon_name: The icon name returned by openweathermap + Format is always 2 numbers followed by 'd' or 'n' as the 3rd character + """ + icon_map = ("01", "02", "03", "04", "09", "10", "11", "13", "50") + if self._icon_group: + self._icon_group.pop() + if icon_name is not None: + row = None + for index, icon in enumerate(icon_map): + if icon == icon_name[0:2]: + row = index + break + column = 0 + if icon_name[2] == "n": + column = 1 + if row is not None: + self._icon_sprite[0] = (row * 2) + column + self._icon_group.append(self._icon_sprite) + # helper function to assign color to minutes labels def get_minutes_color(self, minutes): try: @@ -80,12 +178,48 @@ def get_minutes_color(self, minutes): print("Value Error: {}".format(e)) return metro_orange + # update temperature text, trend, and max/min + # input is a weather dict + def update_weather(self, weather): + if weather: + # set the icon + self.set_icon(weather["icon"]) + + # set the temperature + self.temp_text.text = "%d" % weather["current_temp"] + self.min_temp_text.text = "%d" % weather["daily_temp_min"] + self.max_temp_text.text = "%d" % weather["daily_temp_max"] + + # set temperature trend + # if the temperature change is more than 1 degree + temp_diff_default = 1 + temp_diff = weather["hourly_next_temp"] - weather["current_temp"] + if temp_diff > 0 and temp_diff > temp_diff_default: + # comma is increase arrow + self.temp_trend_icon.text = "," + self.temp_trend_icon.color = metro_red + self.temp_trend_icon.y = self.row1 - 6 + self._temp_trend_group.hidden = False + + elif temp_diff < 0 and abs(temp_diff) > temp_diff_default: + # period is decrease arrow + self.temp_trend_icon.text = "." + self.temp_trend_icon.color = 0x1e81b0 + self.temp_trend_icon.y = self.row1 - 6 + self._temp_trend_group.hidden = False + + else: + self._temp_trend_group.hidden = True + else: + self.temp_text.text = "..." + # update train destination text and time to arrival # input is a list of train objects and display config integer + # TODO abstract default and error handling to support any station def assign_trains(self, trains, historical_trains): try: if trains[0] is not None: - self.top_row_train_text.text = trains[0].destination_name + self.top_row_train_text.text = trains[0].destination # if train isn't Shady Grove, set train text color to white if trains[0].destination_code is not "A15": @@ -99,14 +233,14 @@ def assign_trains(self, trains, historical_trains): # no A train data elif historical_trains[0] is not None: - self.top_row_train_text.text = historical_trains[0].destination_name + self.top_row_train_text.text = historical_trains[0].destination self.top_row_train_min.text = historical_trains[0].minutes self.top_row_train_min.color = 0xFFFFFF else: self.top_row_train_min.text = "NULL" if trains[1] is not None: - self.bottom_row_train_text.text = trains[1].destination_name + self.bottom_row_train_text.text = trains[1].destination # if train isn't Glenmont, set train text color to white if trains[1].destination_code is not "B11": @@ -120,7 +254,7 @@ def assign_trains(self, trains, historical_trains): # no B train data elif historical_trains[1] is not None: - self.bottom_row_train_text.text = historical_trains[1].destination_name + self.bottom_row_train_text.text = historical_trains[1].destination self.bottom_row_train_min.text = historical_trains[1].minutes self.bottom_row_train_min.color = 0xFFFFFF else: