|
| 1 | +# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +import os |
| 6 | +import gc |
| 7 | +import ssl |
| 8 | +import time |
| 9 | +import wifi |
| 10 | +import socketpool |
| 11 | +import adafruit_requests |
| 12 | +import adafruit_display_text.label |
| 13 | +import board |
| 14 | +import terminalio |
| 15 | +import displayio |
| 16 | +import framebufferio |
| 17 | +import rgbmatrix |
| 18 | +import adafruit_json_stream as json_stream |
| 19 | +import microcontroller |
| 20 | +from adafruit_ticks import ticks_ms, ticks_add, ticks_diff |
| 21 | +from adafruit_datetime import datetime, timedelta |
| 22 | +import neopixel |
| 23 | + |
| 24 | +displayio.release_displays() |
| 25 | + |
| 26 | +# font color for text on matrix |
| 27 | +font_color = 0xFFFFFF |
| 28 | +# your timezone UTC offset and timezone name |
| 29 | +timezone_info = [-4, "EDT"] |
| 30 | +# the name of the sports you want to follow |
| 31 | +sport_name = ["football", "baseball", "soccer", "hockey", "basketball"] |
| 32 | +# the name of the corresponding leages you want to follow |
| 33 | +sport_league = ["nfl", "mlb", "usa.1", "nhl", "nba"] |
| 34 | +# the team names you want to follow |
| 35 | +# must match the order of sport/league arrays |
| 36 | +# include full name and then abbreviation (usually city/region) |
| 37 | +team0 = ["New England Patriots", "NE"] |
| 38 | +team1 = ["Boston Red Sox", "BOS"] |
| 39 | +team2 = ["New England Revolution", "NE"] |
| 40 | +team3 = ["Boston Bruins", "BOS"] |
| 41 | +team4 = ["Boston Celtics", "BOS"] |
| 42 | +# how often the API should be fetched |
| 43 | +fetch_timer = 300 # seconds |
| 44 | +# how often the display should update |
| 45 | +display_timer = 30 # seconds |
| 46 | + |
| 47 | +pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness = 0.3, auto_write=True) |
| 48 | + |
| 49 | +# matrix setup |
| 50 | +base_width = 64 |
| 51 | +base_height = 32 |
| 52 | +chain_across = 2 |
| 53 | +tile_down = 2 |
| 54 | +DISPLAY_WIDTH = base_width * chain_across |
| 55 | +DISPLAY_HEIGHT = base_height * tile_down |
| 56 | +matrix = rgbmatrix.RGBMatrix( |
| 57 | + width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, bit_depth=3, |
| 58 | + rgb_pins=[ |
| 59 | + board.MTX_R1, |
| 60 | + board.MTX_G1, |
| 61 | + board.MTX_B1, |
| 62 | + board.MTX_R2, |
| 63 | + board.MTX_G2, |
| 64 | + board.MTX_B2 |
| 65 | + ], |
| 66 | + addr_pins=[ |
| 67 | + board.MTX_ADDRA, |
| 68 | + board.MTX_ADDRB, |
| 69 | + board.MTX_ADDRC, |
| 70 | + board.MTX_ADDRD |
| 71 | + ], |
| 72 | + clock_pin=board.MTX_CLK, |
| 73 | + latch_pin=board.MTX_LAT, |
| 74 | + output_enable_pin=board.MTX_OE, |
| 75 | + tile=tile_down, serpentine=True, |
| 76 | + doublebuffer=False |
| 77 | +) |
| 78 | +display = framebufferio.FramebufferDisplay(matrix) |
| 79 | + |
| 80 | +# connect to WIFI |
| 81 | +wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) |
| 82 | +print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}") |
| 83 | + |
| 84 | +# add API URLs |
| 85 | +SPORT_URLS = [] |
| 86 | +for i in range(5): |
| 87 | + d = ( |
| 88 | + f"https://site.api.espn.com/apis/site/v2/sports/{sport_name[i]}/{sport_league[i]}/scoreboard" |
| 89 | + ) |
| 90 | + SPORT_URLS.append(d) |
| 91 | + |
| 92 | +context = ssl.create_default_context() |
| 93 | +pool = socketpool.SocketPool(wifi.radio) |
| 94 | +requests = adafruit_requests.Session(pool, context) |
| 95 | + |
| 96 | +# arrays for teams, logos and display groups |
| 97 | +teams = [] |
| 98 | +logos = [] |
| 99 | +groups = [] |
| 100 | +# add team to array |
| 101 | +teams.append(team0) |
| 102 | +# grab logo bitmap name |
| 103 | +logo0 = "/team0_logos/" + team0[1] + ".bmp" |
| 104 | +# add logo to array |
| 105 | +logos.append(logo0) |
| 106 | +# create a display group |
| 107 | +group0 = displayio.Group() |
| 108 | +# add group to array |
| 109 | +groups.append(group0) |
| 110 | +# repeat: |
| 111 | +teams.append(team1) |
| 112 | +logo1 = "/team1_logos/" + team1[1] + ".bmp" |
| 113 | +logos.append(logo1) |
| 114 | +group1 = displayio.Group() |
| 115 | +groups.append(group1) |
| 116 | +teams.append(team2) |
| 117 | +logo2 = "/team2_logos/" + team2[1] + ".bmp" |
| 118 | +logos.append(logo2) |
| 119 | +group2 = displayio.Group() |
| 120 | +groups.append(group2) |
| 121 | +teams.append(team3) |
| 122 | +logo3 = "/team3_logos/" + team3[1] + ".bmp" |
| 123 | +logos.append(logo3) |
| 124 | +group3 = displayio.Group() |
| 125 | +groups.append(group3) |
| 126 | +teams.append(team4) |
| 127 | +logo4 = "/team4_logos/" + team4[1] + ".bmp" |
| 128 | +logos.append(logo4) |
| 129 | +group4 = displayio.Group() |
| 130 | +groups.append(group4) |
| 131 | + |
| 132 | +# initial startup screen |
| 133 | +# shows the five team logos you are following |
| 134 | +def sport_startup(logo): |
| 135 | + try: |
| 136 | + group = displayio.Group() |
| 137 | + bitmap0 = displayio.OnDiskBitmap(logo[0]) |
| 138 | + grid0 = displayio.TileGrid(bitmap0, pixel_shader=bitmap0.pixel_shader, x = 0) |
| 139 | + bitmap1 = displayio.OnDiskBitmap(logo[1]) |
| 140 | + grid1 = displayio.TileGrid(bitmap1, pixel_shader=bitmap1.pixel_shader, x = 32) |
| 141 | + bitmap2 = displayio.OnDiskBitmap(logo[2]) |
| 142 | + grid2 = displayio.TileGrid(bitmap2, pixel_shader=bitmap2.pixel_shader, x = 64) |
| 143 | + bitmap3 = displayio.OnDiskBitmap(logo[3]) |
| 144 | + grid3 = displayio.TileGrid(bitmap3, pixel_shader=bitmap3.pixel_shader, x = 96) |
| 145 | + bitmap4 = displayio.OnDiskBitmap(logo[4]) |
| 146 | + grid4 = displayio.TileGrid(bitmap4, pixel_shader=bitmap4.pixel_shader, x = 48, y=32) |
| 147 | + group.append(grid0) |
| 148 | + group.append(grid1) |
| 149 | + group.append(grid2) |
| 150 | + group.append(grid3) |
| 151 | + group.append(grid4) |
| 152 | + display.show(group) |
| 153 | + # pylint: disable=broad-except |
| 154 | + except Exception: |
| 155 | + print("Can't find bitmap. Did you run the get_team_logos.py script?") |
| 156 | + |
| 157 | +# takes UTC time from JSON and reformats how its displayed |
| 158 | +def convert_date_format(date, tz_information): |
| 159 | + # Manually extract year, month, day, hour, and minute from the string |
| 160 | + year = int(date[0:4]) |
| 161 | + month = int(date[5:7]) |
| 162 | + day = int(date[8:10]) |
| 163 | + hour = int(date[11:13]) |
| 164 | + minute = int(date[14:16]) |
| 165 | + # Construct a datetime object using the extracted values |
| 166 | + dt = datetime(year, month, day, hour, minute) |
| 167 | + # Adjust the datetime object for the target timezone offset |
| 168 | + dt_adjusted = dt + timedelta(hours=tz_information[0]) |
| 169 | + # Extract fields for output format |
| 170 | + month = dt_adjusted.month |
| 171 | + day = dt_adjusted.day |
| 172 | + hour = dt_adjusted.hour |
| 173 | + minute = dt_adjusted.minute |
| 174 | + # Convert 24-hour format to 12-hour format and determine AM/PM |
| 175 | + am_pm = "AM" if hour < 12 else "PM" |
| 176 | + hour_12 = hour if hour <= 12 else hour - 12 |
| 177 | + # Determine the timezone abbreviation based on the offset |
| 178 | + time_zone_str = tz_information[1] |
| 179 | + return f"{month}/{day} - {hour_12}:{minute} {am_pm} {time_zone_str}" |
| 180 | + |
| 181 | +# the actual API and display function |
| 182 | +# pylint: disable=too-many-locals, too-many-branches, too-many-statements |
| 183 | +def get_data(data, team, logo, group): |
| 184 | + pixel.fill((0, 0, 255)) |
| 185 | + print(f"Fetching data from {data}") |
| 186 | + playing = False |
| 187 | + names = [] |
| 188 | + scores = [] |
| 189 | + info = [] |
| 190 | + # the team you are following's logo |
| 191 | + bitmap0 = displayio.OnDiskBitmap(logo) |
| 192 | + grid0 = displayio.TileGrid(bitmap0, pixel_shader=bitmap0.pixel_shader, x = 2) |
| 193 | + home_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color, |
| 194 | + text=" ") |
| 195 | + away_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color, |
| 196 | + text=" ") |
| 197 | + vs_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color, |
| 198 | + text=" ") |
| 199 | + vs_text.anchor_point = (0.5, 0.0) |
| 200 | + vs_text.anchored_position = (DISPLAY_WIDTH / 2, 14) |
| 201 | + info_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color, |
| 202 | + text=" ") |
| 203 | + info_text.anchor_point = (0.5, 1.0) |
| 204 | + info_text.anchored_position = (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT) |
| 205 | + # make the request to the API |
| 206 | + resp = requests.get(data) |
| 207 | + # stream the json |
| 208 | + json_data = json_stream.load(resp.iter_content(32)) |
| 209 | + for event in json_data["events"]: |
| 210 | + # clear the date and then add the date to the array |
| 211 | + # the date for your game will remain |
| 212 | + info.clear() |
| 213 | + info.append(event["date"]) |
| 214 | + # check for your team playing |
| 215 | + if team[0] not in event["name"]: |
| 216 | + continue |
| 217 | + for competition in event["competitions"]: |
| 218 | + for competitor in competition["competitors"]: |
| 219 | + # if your team is playing: |
| 220 | + playing = True |
| 221 | + # get team names |
| 222 | + # index indicates home vs. away |
| 223 | + names.append(competitor["team"]["abbreviation"]) |
| 224 | + # the current score |
| 225 | + scores.append(competitor["score"]) |
| 226 | + # gets info on game |
| 227 | + info.append(event["status"]["type"]["shortDetail"]) |
| 228 | + break |
| 229 | + # debug printing |
| 230 | + print(names) |
| 231 | + print(scores) |
| 232 | + print(info) |
| 233 | + if playing: |
| 234 | + # pull out the date |
| 235 | + date = info[0] |
| 236 | + # convert it to be readable |
| 237 | + date = convert_date_format(date, timezone_info) |
| 238 | + print(date) |
| 239 | + # pull out the info |
| 240 | + info = info[1] |
| 241 | + # check if it's pre-game |
| 242 | + if str(info) == date or str(info) == "Scheduled": |
| 243 | + status = "pre" |
| 244 | + print("match, pre-game") |
| 245 | + else: |
| 246 | + status = info |
| 247 | + # home and away text |
| 248 | + # teams index determines which team is home or away |
| 249 | + home_text.text="HOME" |
| 250 | + away_text.text="AWAY" |
| 251 | + if team[1] is names[0]: |
| 252 | + home_game = True |
| 253 | + home_text.anchor_point = (0.0, 0.5) |
| 254 | + home_text.anchored_position = (5, 37) |
| 255 | + away_text.anchor_point = (1.0, 0.5) |
| 256 | + away_text.anchored_position = (124, 37) |
| 257 | + vs_team = names[1] |
| 258 | + else: |
| 259 | + home_game = False |
| 260 | + away_text.anchor_point = (0.0, 0.5) |
| 261 | + away_text.anchored_position = (5, 37) |
| 262 | + home_text.anchor_point = (1.0, 0.5) |
| 263 | + home_text.anchored_position = (124, 37) |
| 264 | + vs_team = names[0] |
| 265 | + # if it's pre-game, show "VS" |
| 266 | + if status == "pre": |
| 267 | + vs_text.text="VS" |
| 268 | + info_text.text=date |
| 269 | + # if it's active or final show score |
| 270 | + else: |
| 271 | + info_text.text=info |
| 272 | + if home_game: |
| 273 | + vs_text.text=f"{scores[0]} - {scores[1]}" |
| 274 | + else: |
| 275 | + vs_text.text=f"{scores[1]} - {scores[0]}" |
| 276 | + # load in logo from other team |
| 277 | + vs_logo = logo.replace(team[1], vs_team) |
| 278 | + # if there is no game matching your team: |
| 279 | + else: |
| 280 | + status = "pre" |
| 281 | + vs_logo = logo |
| 282 | + info_text.text="NO DATA AVAILABLE" |
| 283 | + # load in the other team's logo |
| 284 | + bitmap1 = displayio.OnDiskBitmap(vs_logo) |
| 285 | + grid1 = displayio.TileGrid(bitmap1, pixel_shader=bitmap1.pixel_shader, x = 94) |
| 286 | + print("done") |
| 287 | + # update the display group. try/except in case its the first time it's being added |
| 288 | + try: |
| 289 | + group[0] = grid0 |
| 290 | + group[1] = grid1 |
| 291 | + group[2] = home_text |
| 292 | + group[3] = away_text |
| 293 | + group[4] = vs_text |
| 294 | + group[5] = info_text |
| 295 | + except IndexError: |
| 296 | + group.append(grid0) |
| 297 | + group.append(grid1) |
| 298 | + group.append(home_text) |
| 299 | + group.append(away_text) |
| 300 | + group.append(vs_text) |
| 301 | + group.append(info_text) |
| 302 | + # close the response |
| 303 | + resp.close() |
| 304 | + pixel.fill((0, 0, 0)) |
| 305 | + # return that data was just fetched |
| 306 | + fetch_status = True |
| 307 | + return fetch_status |
| 308 | + |
| 309 | +# index and clock for fetching |
| 310 | +fetch_index = 0 |
| 311 | +fetch_timer = fetch_timer * 1000 |
| 312 | +# index and clock for updating display |
| 313 | +display_index = 0 |
| 314 | +display_timer = display_timer * 1000 |
| 315 | +# load logos |
| 316 | +sport_startup(logos) |
| 317 | +# initial data fetch |
| 318 | +for z in range(5): |
| 319 | + try: |
| 320 | + just_fetched = get_data(SPORT_URLS[z], |
| 321 | + teams[z], |
| 322 | + logos[z], |
| 323 | + groups[z]) |
| 324 | + display.show(groups[z]) |
| 325 | + # pylint: disable=broad-except |
| 326 | + except Exception as Error: |
| 327 | + print(Error) |
| 328 | + time.sleep(10) |
| 329 | + gc.collect() |
| 330 | + time.sleep(5) |
| 331 | + microcontroller.reset() |
| 332 | +# start clocks |
| 333 | +just_fetched = True |
| 334 | +fetch_clock = ticks_ms() |
| 335 | +display_clock = ticks_ms() |
| 336 | + |
| 337 | +while True: |
| 338 | + try: |
| 339 | + if not just_fetched: |
| 340 | + # garbage collection for display groups |
| 341 | + gc.collect() |
| 342 | + # fetch the json for the next team |
| 343 | + just_fetched = get_data(SPORT_URLS[fetch_index], |
| 344 | + teams[fetch_index], |
| 345 | + logos[fetch_index], |
| 346 | + groups[fetch_index]) |
| 347 | + # advance index |
| 348 | + fetch_index = (fetch_index + 1) % len(teams) |
| 349 | + # reset clocks |
| 350 | + fetch_clock = ticks_add(fetch_clock, fetch_timer) |
| 351 | + display_clock = ticks_add(display_clock, display_timer) |
| 352 | + # update display seperate from API request |
| 353 | + if ticks_diff(ticks_ms(), display_clock) >= display_timer: |
| 354 | + print("updating display") |
| 355 | + display.show(groups[display_index]) |
| 356 | + display_index = (display_index + 1) % len(teams) |
| 357 | + display_clock = ticks_add(display_clock, display_timer) |
| 358 | + # cleared for fetching after time has passed |
| 359 | + if ticks_diff(ticks_ms(), fetch_clock) >= fetch_timer: |
| 360 | + just_fetched = False |
| 361 | + # pylint: disable=broad-except |
| 362 | + except Exception as Error: |
| 363 | + print(Error) |
| 364 | + time.sleep(10) |
| 365 | + gc.collect() |
| 366 | + time.sleep(5) |
| 367 | + microcontroller.reset() |
0 commit comments