Skip to content

Commit b92b2f4

Browse files
authored
Merge pull request #2623 from adafruit/sportball
adding code for the ESPN API project
2 parents 74f5d77 + 3886113 commit b92b2f4

File tree

2 files changed

+497
-0
lines changed

2 files changed

+497
-0
lines changed
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
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

Comments
 (0)