format with ruff
This commit is contained in:
+3
-1
@@ -7,13 +7,14 @@ LEAGUES = [
|
|||||||
("hockey", "nhl"),
|
("hockey", "nhl"),
|
||||||
("football", "nfl"),
|
("football", "nfl"),
|
||||||
("basketball", "nba"),
|
("basketball", "nba"),
|
||||||
("baseball", "mlb")
|
("baseball", "mlb"),
|
||||||
]
|
]
|
||||||
|
|
||||||
LOGO_SIZE = (16, 16)
|
LOGO_SIZE = (16, 16)
|
||||||
|
|
||||||
os.makedirs("./assets/logos", exist_ok=True)
|
os.makedirs("./assets/logos", exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def download_logos(sport, league):
|
def download_logos(sport, league):
|
||||||
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/teams"
|
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/teams"
|
||||||
resp = requests.get(url, timeout=10)
|
resp = requests.get(url, timeout=10)
|
||||||
@@ -49,6 +50,7 @@ def download_logos(sport, league):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error downloading {abbr}: {e}")
|
print(f"Error downloading {abbr}: {e}")
|
||||||
|
|
||||||
|
|
||||||
for sport, league in LEAGUES:
|
for sport, league in LEAGUES:
|
||||||
print(f"\nDownloading {league.upper()} logos...")
|
print(f"\nDownloading {league.upper()} logos...")
|
||||||
download_logos(sport, league)
|
download_logos(sport, league)
|
||||||
|
|||||||
@@ -5,31 +5,29 @@ import json
|
|||||||
|
|
||||||
GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
|
GOVEE_API_BASE_URL = "https://openapi.api.govee.com/router/api/v1/"
|
||||||
|
|
||||||
|
|
||||||
class GoveeApi:
|
class GoveeApi:
|
||||||
key = ""
|
key = ""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.headers = {
|
self.headers = {"Govee-API-Key": key}
|
||||||
'Govee-API-Key': key
|
|
||||||
}
|
|
||||||
|
|
||||||
def __get_random_string(self):
|
def __get_random_string(self):
|
||||||
characters = string.ascii_letters + string.digits
|
characters = string.ascii_letters + string.digits
|
||||||
result_str = ''.join(random.choices(characters, k=4))
|
result_str = "".join(random.choices(characters, k=4))
|
||||||
return result_str
|
return result_str
|
||||||
|
|
||||||
def get_diy_scenes(self, sku, device):
|
def get_diy_scenes(self, sku, device):
|
||||||
payload = {
|
payload = {
|
||||||
"requestId": self.__get_random_string(),
|
"requestId": self.__get_random_string(),
|
||||||
"payload": {
|
"payload": {"sku": sku, "device": device},
|
||||||
"sku": sku,
|
|
||||||
"device": device
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res = requests.post(GOVEE_API_BASE_URL + 'device/diy-scenes', json=payload, headers=self.headers)
|
res = requests.post(
|
||||||
|
GOVEE_API_BASE_URL + "device/diy-scenes", json=payload, headers=self.headers
|
||||||
|
)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
|
print("[GOVEE] DIY scene fetch: " + json.dumps(res.json(), indent=4))
|
||||||
@@ -38,19 +36,21 @@ class GoveeApi:
|
|||||||
|
|
||||||
def set_diy_scene(self, sku, device):
|
def set_diy_scene(self, sku, device):
|
||||||
payload = {
|
payload = {
|
||||||
'requestId': self.__get_random_string(),
|
"requestId": self.__get_random_string(),
|
||||||
'payload': {
|
"payload": {
|
||||||
'sku': sku,
|
"sku": sku,
|
||||||
'device': device,
|
"device": device,
|
||||||
'capability': {
|
"capability": {
|
||||||
'type': 'device.capabilities.dynamic_scene',
|
"type": "device.capabilities.dynamic_scene",
|
||||||
'instance': 'diyScene',
|
"instance": "diyScene",
|
||||||
'value': 22757907
|
"value": 22757907,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res = requests.post(GOVEE_API_BASE_URL + 'device/control', headers=self.headers, json=payload)
|
res = requests.post(
|
||||||
|
GOVEE_API_BASE_URL + "device/control", headers=self.headers, json=payload
|
||||||
|
)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
print("[GOVEE] Set DIY scene: " + json.dumps(res.json(), indent=4))
|
print("[GOVEE] Set DIY scene: " + json.dumps(res.json(), indent=4))
|
||||||
@@ -59,19 +59,21 @@ class GoveeApi:
|
|||||||
|
|
||||||
def set_to_original_color(self, sku, device):
|
def set_to_original_color(self, sku, device):
|
||||||
payload = {
|
payload = {
|
||||||
'requestId': self.__get_random_string(),
|
"requestId": self.__get_random_string(),
|
||||||
'payload': {
|
"payload": {
|
||||||
'sku': sku,
|
"sku": sku,
|
||||||
'device': device,
|
"device": device,
|
||||||
'capability': {
|
"capability": {
|
||||||
'type': 'devices.capabilities.color_setting',
|
"type": "devices.capabilities.color_setting",
|
||||||
'instance': 'colorRgb',
|
"instance": "colorRgb",
|
||||||
'value': 16711680
|
"value": 16711680,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res = requests.post(GOVEE_API_BASE_URL + 'device/control', json=payload, headers=self.headers)
|
res = requests.post(
|
||||||
|
GOVEE_API_BASE_URL + "device/control", json=payload, headers=self.headers
|
||||||
|
)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4))
|
print("[GOVEE] Set to original: " + json.dumps(res.json(), indent=4))
|
||||||
|
|||||||
+87
-45
@@ -13,7 +13,7 @@ load_dotenv()
|
|||||||
|
|
||||||
# --- Default vars ---
|
# --- Default vars ---
|
||||||
ASSET_DIR = "./assets"
|
ASSET_DIR = "./assets"
|
||||||
LOGO_DIR = os.path.join(ASSET_DIR, 'logos')
|
LOGO_DIR = os.path.join(ASSET_DIR, "logos")
|
||||||
|
|
||||||
# --- Matrix config ---
|
# --- Matrix config ---
|
||||||
options = RGBMatrixOptions()
|
options = RGBMatrixOptions()
|
||||||
@@ -21,7 +21,7 @@ options.rows = 32
|
|||||||
options.cols = 64
|
options.cols = 64
|
||||||
options.chain_length = 4
|
options.chain_length = 4
|
||||||
options.parallel = 1
|
options.parallel = 1
|
||||||
options.hardware_mapping = 'regular'
|
options.hardware_mapping = "regular"
|
||||||
options.gpio_slowdown = 5
|
options.gpio_slowdown = 5
|
||||||
options.disable_hardware_pulsing = True
|
options.disable_hardware_pulsing = True
|
||||||
options.brightness = 80
|
options.brightness = 80
|
||||||
@@ -33,18 +33,19 @@ canvas = matrix.CreateFrameCanvas()
|
|||||||
font = graphics.Font()
|
font = graphics.Font()
|
||||||
font_small = graphics.Font()
|
font_small = graphics.Font()
|
||||||
font_big = graphics.Font()
|
font_big = graphics.Font()
|
||||||
font.LoadFont(os.path.join(ASSET_DIR, 'fonts/7x13.bdf'))
|
font.LoadFont(os.path.join(ASSET_DIR, "fonts/7x13.bdf"))
|
||||||
font_small.LoadFont(os.path.join(ASSET_DIR, 'fonts/5x7.bdf'))
|
font_small.LoadFont(os.path.join(ASSET_DIR, "fonts/5x7.bdf"))
|
||||||
|
|
||||||
# try to load a big font for GOAL!, fall back to regular if not available
|
# try to load a big font for GOAL!, fall back to regular if not available
|
||||||
try:
|
try:
|
||||||
font_big.LoadFont(os.path.join(ASSET_DIR, 'fonts/9x18.bdf'))
|
font_big.LoadFont(os.path.join(ASSET_DIR, "fonts/9x18.bdf"))
|
||||||
except:
|
except:
|
||||||
font_big = font
|
font_big = font
|
||||||
|
|
||||||
# --- Logo cache ---
|
# --- Logo cache ---
|
||||||
logo_cache = {}
|
logo_cache = {}
|
||||||
|
|
||||||
|
|
||||||
# --- Pre-built colors ---
|
# --- Pre-built colors ---
|
||||||
class Colors(Enum):
|
class Colors(Enum):
|
||||||
WHITE = (255, 255, 255)
|
WHITE = (255, 255, 255)
|
||||||
@@ -53,12 +54,14 @@ class Colors(Enum):
|
|||||||
SABRES_BLUE = (0, 135, 48)
|
SABRES_BLUE = (0, 135, 48)
|
||||||
SABRES_GOLD = (252, 20, 210)
|
SABRES_GOLD = (252, 20, 210)
|
||||||
|
|
||||||
|
|
||||||
# --- Govee API ---
|
# --- Govee API ---
|
||||||
govee_api = govee.GoveeApi(key=os.environ['GOVEE_API_KEY'])
|
govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"])
|
||||||
|
|
||||||
# --- PyGame Audio ---
|
# --- PyGame Audio ---
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
|
|
||||||
|
|
||||||
# --- Goal celebrations ---
|
# --- Goal celebrations ---
|
||||||
def render_goal_frame(text, text_scale, bg_color, text_color):
|
def render_goal_frame(text, text_scale, bg_color, text_color):
|
||||||
big_h = max(8, int(32 * text_scale))
|
big_h = max(8, int(32 * text_scale))
|
||||||
@@ -67,7 +70,9 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
|
|||||||
|
|
||||||
# rpi specific, fall back to default font if not existing
|
# rpi specific, fall back to default font if not existing
|
||||||
try:
|
try:
|
||||||
pil_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h)
|
pil_font = ImageFont.truetype(
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pil_font = ImageFont.load_default()
|
pil_font = ImageFont.load_default()
|
||||||
|
|
||||||
@@ -114,6 +119,7 @@ def render_goal_frame(text, text_scale, bg_color, text_color):
|
|||||||
|
|
||||||
return scaled
|
return scaled
|
||||||
|
|
||||||
|
|
||||||
def play_goal_celebration(text, color1, color2):
|
def play_goal_celebration(text, color1, color2):
|
||||||
global canvas
|
global canvas
|
||||||
|
|
||||||
@@ -153,7 +159,9 @@ def play_goal_celebration(text, color1, color2):
|
|||||||
# Phase 4: white flash to end
|
# Phase 4: white flash to end
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
canvas.Clear()
|
canvas.Clear()
|
||||||
frame = render_goal_frame(text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value)
|
frame = render_goal_frame(
|
||||||
|
text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value
|
||||||
|
)
|
||||||
draw_pil_image(canvas, frame)
|
draw_pil_image(canvas, frame)
|
||||||
canvas = matrix.SwapOnVSync(canvas)
|
canvas = matrix.SwapOnVSync(canvas)
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
@@ -168,10 +176,12 @@ def play_goal_celebration(text, color1, color2):
|
|||||||
# Hold for a moment then return to scoreboard
|
# Hold for a moment then return to scoreboard
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
def play_audio(filename):
|
def play_audio(filename):
|
||||||
pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
|
pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
|
||||||
pygame.mixer.music.play()
|
pygame.mixer.music.play()
|
||||||
|
|
||||||
|
|
||||||
# --- Utilities ---
|
# --- Utilities ---
|
||||||
def draw_pil_image(canvas, img):
|
def draw_pil_image(canvas, img):
|
||||||
for x in range(img.width):
|
for x in range(img.width):
|
||||||
@@ -179,6 +189,7 @@ def draw_pil_image(canvas, img):
|
|||||||
r, g, b = img.getpixel((x, y))
|
r, g, b = img.getpixel((x, y))
|
||||||
canvas.SetPixel(x, y, b, g, r) # bgr panels
|
canvas.SetPixel(x, y, b, g, r) # bgr panels
|
||||||
|
|
||||||
|
|
||||||
def load_logo(league, abbr):
|
def load_logo(league, abbr):
|
||||||
key = f"{league}_{abbr}"
|
key = f"{league}_{abbr}"
|
||||||
if key in logo_cache:
|
if key in logo_cache:
|
||||||
@@ -199,6 +210,7 @@ def load_logo(league, abbr):
|
|||||||
logo_cache[key] = None
|
logo_cache[key] = None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def draw_logo(canvas, img, x, y):
|
def draw_logo(canvas, img, x, y):
|
||||||
if img is None:
|
if img is None:
|
||||||
return
|
return
|
||||||
@@ -207,6 +219,7 @@ def draw_logo(canvas, img, x, y):
|
|||||||
r, g, b = img.getpixel((px, py))
|
r, g, b = img.getpixel((px, py))
|
||||||
canvas.SetPixel(x + px, y + py, r, g, b) # bgr panels
|
canvas.SetPixel(x + px, y + py, r, g, b) # bgr panels
|
||||||
|
|
||||||
|
|
||||||
# --- Fetch scores ---
|
# --- Fetch scores ---
|
||||||
def get_scores(sport, league):
|
def get_scores(sport, league):
|
||||||
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard"
|
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard"
|
||||||
@@ -220,7 +233,8 @@ def get_scores(sport, league):
|
|||||||
home = next(t for t in teams if t["homeAway"] == "home")
|
home = next(t for t in teams if t["homeAway"] == "home")
|
||||||
away = next(t for t in teams if t["homeAway"] == "away")
|
away = next(t for t in teams if t["homeAway"] == "away")
|
||||||
status = event["status"]["type"]["shortDetail"]
|
status = event["status"]["type"]["shortDetail"]
|
||||||
games.append({
|
games.append(
|
||||||
|
{
|
||||||
"league": league,
|
"league": league,
|
||||||
"away": away["team"]["abbreviation"].upper(),
|
"away": away["team"]["abbreviation"].upper(),
|
||||||
"away_score": away["score"],
|
"away_score": away["score"],
|
||||||
@@ -228,12 +242,14 @@ def get_scores(sport, league):
|
|||||||
"home_score": home["score"],
|
"home_score": home["score"],
|
||||||
"status": status,
|
"status": status,
|
||||||
"id": event["id"],
|
"id": event["id"],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return games
|
return games
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fetch error ({league}): {e}")
|
print(f"Fetch error ({league}): {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_all_scores():
|
def get_all_scores():
|
||||||
games = []
|
games = []
|
||||||
games += get_scores("hockey", "nhl")
|
games += get_scores("hockey", "nhl")
|
||||||
@@ -242,6 +258,7 @@ def get_all_scores():
|
|||||||
games += get_scores("baseball", "mlb")
|
games += get_scores("baseball", "mlb")
|
||||||
return games
|
return games
|
||||||
|
|
||||||
|
|
||||||
# --- Draw all games across all panels ---
|
# --- Draw all games across all panels ---
|
||||||
def draw_all_games(canvas, games, start_index):
|
def draw_all_games(canvas, games, start_index):
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
@@ -256,16 +273,45 @@ def draw_all_games(canvas, games, start_index):
|
|||||||
draw_logo(canvas, away_logo, offset + 0, 0)
|
draw_logo(canvas, away_logo, offset + 0, 0)
|
||||||
draw_logo(canvas, home_logo, offset + 0, 16)
|
draw_logo(canvas, home_logo, offset + 0, 16)
|
||||||
|
|
||||||
graphics.DrawText(canvas, font_small, offset + 18, 11, graphics.Color(255,255,255), game["away"])
|
graphics.DrawText(
|
||||||
graphics.DrawText(canvas, font_small, offset + 18, 27, graphics.Color(255,255,255), game["home"])
|
canvas,
|
||||||
|
font_small,
|
||||||
|
offset + 18,
|
||||||
|
11,
|
||||||
|
graphics.Color(255, 255, 255),
|
||||||
|
game["away"],
|
||||||
|
)
|
||||||
|
graphics.DrawText(
|
||||||
|
canvas,
|
||||||
|
font_small,
|
||||||
|
offset + 18,
|
||||||
|
27,
|
||||||
|
graphics.Color(255, 255, 255),
|
||||||
|
game["home"],
|
||||||
|
)
|
||||||
|
|
||||||
graphics.DrawText(canvas, font, offset + 40, 13, graphics.Color(255,255,255), str(game["away_score"]))
|
graphics.DrawText(
|
||||||
graphics.DrawText(canvas, font, offset + 40, 29, graphics.Color(255,255,255), str(game["home_score"]))
|
canvas,
|
||||||
|
font,
|
||||||
|
offset + 40,
|
||||||
|
13,
|
||||||
|
graphics.Color(255, 255, 255),
|
||||||
|
str(game["away_score"]),
|
||||||
|
)
|
||||||
|
graphics.DrawText(
|
||||||
|
canvas,
|
||||||
|
font,
|
||||||
|
offset + 40,
|
||||||
|
29,
|
||||||
|
graphics.Color(255, 255, 255),
|
||||||
|
str(game["home_score"]),
|
||||||
|
)
|
||||||
|
|
||||||
if i < 3:
|
if i < 3:
|
||||||
for row in range(32):
|
for row in range(32):
|
||||||
canvas.SetPixel(offset + 63, row, 40, 40, 40)
|
canvas.SetPixel(offset + 63, row, 40, 40, 40)
|
||||||
|
|
||||||
|
|
||||||
# --- Main loop ---
|
# --- Main loop ---
|
||||||
def run():
|
def run():
|
||||||
global canvas
|
global canvas
|
||||||
@@ -282,47 +328,43 @@ def run():
|
|||||||
print(game.status, game.home, game.away)
|
print(game.status, game.home, game.away)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
# now = time()
|
|
||||||
|
|
||||||
# if now - last_fetch > 30:
|
|
||||||
# new_games = get_all_scores()
|
|
||||||
|
|
||||||
# # update prev_scores
|
|
||||||
# for game in new_games:
|
|
||||||
# try:
|
|
||||||
# prev_scores[gid] = (int(game["away_score"]), int(game["home_score"]))
|
|
||||||
# except ValueError:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# games = new_games
|
|
||||||
# last_fetch = now
|
|
||||||
# if not games:
|
|
||||||
# current_page = 0
|
|
||||||
|
|
||||||
# if games and now - last_switch > page_display_time:
|
|
||||||
# current_page = (current_page + 4) % max(len(games), 1)
|
|
||||||
# last_switch = now
|
|
||||||
|
|
||||||
|
|
||||||
# if games:
|
|
||||||
# draw_all_games(canvas, games, current_page)
|
|
||||||
# else:
|
|
||||||
# graphics.DrawText(canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today")
|
|
||||||
|
|
||||||
# canvas = matrix.SwapOnVSync(canvas)
|
|
||||||
# sleep(0.03)
|
|
||||||
now = time()
|
now = time()
|
||||||
|
|
||||||
|
if now - last_fetch > 30:
|
||||||
|
new_games = get_all_scores()
|
||||||
|
|
||||||
|
# update prev_scores
|
||||||
|
for game in new_games:
|
||||||
gid = game["id"]
|
gid = game["id"]
|
||||||
|
try:
|
||||||
|
prev_scores[gid] = (
|
||||||
|
int(game["away_score"]),
|
||||||
|
int(game["home_score"]),
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
games = new_games
|
||||||
|
last_fetch = now
|
||||||
|
if not games:
|
||||||
|
current_page = 0
|
||||||
|
|
||||||
|
if games and now - last_switch > page_display_time:
|
||||||
current_page = (current_page + 4) % max(len(games), 1)
|
current_page = (current_page + 4) % max(len(games), 1)
|
||||||
|
last_switch = now
|
||||||
|
|
||||||
|
canvas.Clear()
|
||||||
|
|
||||||
if games:
|
if games:
|
||||||
draw_all_games(canvas, games, current_page)
|
draw_all_games(canvas, games, current_page)
|
||||||
else:
|
else:
|
||||||
graphics.DrawText(canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today")
|
graphics.DrawText(
|
||||||
|
canvas, font, 10, 22, graphics.Color(Colors.RED), "No games today"
|
||||||
|
)
|
||||||
|
|
||||||
canvas = matrix.SwapOnVSync(canvas)
|
canvas = matrix.SwapOnVSync(canvas)
|
||||||
sleep(0.03)
|
sleep(0.03)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
Reference in New Issue
Block a user