@@ -0,0 +1,283 @@
import requests
from PIL import Image
from rgbmatrix import graphics
import utils . logos as logos
from utils . vars import Colors , font , font_small , PANEL_WIDTH , PANEL_HEIGHT , FANTASY_WIDTH , DIVIDER_COLOR
from time import time
FETCH_INTERVAL = 60 # seconds between API calls
# Logo layout: 14x14 centered vertically in the 32px panel
LOGO_SIZE = 14
LOGO_Y = 9 # (32 - 14) // 2 = 9
TEXT_X = 17 # x offset for text after logo
# Off-enum colors
_COLOR_GREEN = ( 0 , 200 , 80 )
# --- State ---
_data = None
_last_fetch = 0
_cells = [ ]
_scroll_x = 0
_scroll_speed = 1
_frames_per_tick = 2
_tick = 0
_virtual_canvas = None
_virtual_dirty = True
# ---------------------------------------------------------------------------
# RBG channel correction for Waveshare P2.5 panels
# Hardware physical order is R-B-G, so swap G and B before passing to API.
# ---------------------------------------------------------------------------
def _rbg ( rgb ) :
r , g , b = rgb
return graphics . Color ( r , b , g )
# ---------------------------------------------------------------------------
# Data fetch
# ---------------------------------------------------------------------------
def _fetch ( ) :
try :
resp = requests . get ( " https://api.alexav.gg/v4/sports/fantasy " , timeout = 5 )
resp . raise_for_status ( )
return resp . json ( )
except Exception as e :
print ( f " [fantasy] fetch error: { e } " )
return None
# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------
def _player_name ( player ) :
""" Return a short display name (≤9 chars). DEF units use team abbreviation. """
if player . get ( " position " ) == " DEF " :
return player . get ( " team " , " DEF " ) [ : 9 ]
first = player . get ( " first_name " , " " )
last = player . get ( " last_name " , " " )
if first and last :
return f " { first [ 0 ] } . { last } " [ : 9 ]
return ( last or first ) [ : 9 ]
def _team_label ( name , maxlen = 12 ) :
""" Strip parenthetical suffixes and truncate team name. """
paren = name . find ( " ( " )
label = name [ : paren ] if paren > 0 else name
return label [ : maxlen ]
def _fmt_pts ( pts ) :
""" Format fantasy points to one decimal place. """
return f " { float ( pts ) : .1f } "
# ---------------------------------------------------------------------------
# Build the ordered cell list from API data
# Structure: [Team1 Header] [T1 Player ...] [Team2 Header] [T2 Player ...]
# ---------------------------------------------------------------------------
def _build_cells ( data ) :
cells = [ ]
t1 , t2 = data [ " team1 " ] , data [ " team2 " ]
for team , opp in [ ( t1 , t2 ) , ( t2 , t1 ) ] :
meta = team [ " owner " ] . get ( " metadata " , { } )
team_name = meta . get ( " team_name " , team [ " owner " ] [ " display_name " ] )
winning = float ( team [ " points " ] ) > = float ( opp [ " points " ] )
# ── Team summary header cell ──────────────────────────────────────
cells . append ( {
" type " : " team_info " ,
" owner " : team [ " owner " ] [ " display_name " ] ,
" team_name " : team_name ,
" points " : team [ " points " ] ,
" opp_points " : opp [ " points " ] ,
" winning " : winning ,
} )
# ── One cell per starter ─────────────────────────────────────────
pp = team [ " players_points " ]
for player in team [ " starters " ] :
pid = player [ " player_id " ]
injury = player . get ( " injury_status " )
cells . append ( {
" type " : " player " ,
" nfl_team " : player . get ( " team " , " " ) ,
" name " : _player_name ( player ) ,
" position " : player . get ( " position " , " " ) ,
" points " : pp . get ( pid , 0 ) ,
" injury " : injury ,
} )
return cells
# ---------------------------------------------------------------------------
# PIL canvas: logos + dividers (blit-able background layer)
# ---------------------------------------------------------------------------
def _build_virtual_canvas ( cells ) :
if not cells :
return None
n = len ( cells )
total_w = FANTASY_WIDTH * ( n + 4 ) # +4 tail so wrap never shows black
img = Image . new ( " RGB " , ( total_w , PANEL_HEIGHT ) , ( 0 , 0 , 0 ) )
for i , cell in enumerate ( cells * 2 ) :
if i > = n + 4 :
break
_render_pil ( img , cell , i * FANTASY_WIDTH )
return img , n
def _render_pil ( img , cell , x ) :
# right-edge divider
for y in range ( PANEL_HEIGHT ) :
img . putpixel ( ( x + FANTASY_WIDTH - 1 , y ) , DIVIDER_COLOR )
# NFL team logo for player cells
if cell [ " type " ] == " player " and cell [ " nfl_team " ] :
logo = logos . load_logo ( " nfl " , cell [ " nfl_team " ] )
if logo :
img . paste ( logo . resize ( ( LOGO_SIZE , LOGO_SIZE ) ) , ( x + 1 , LOGO_Y ) )
# ---------------------------------------------------------------------------
# Blit a PANEL_WIDTH-wide slice from the PIL canvas to the matrix
# ---------------------------------------------------------------------------
def _blit_slice ( canvas , img , offset ) :
w = img . width
for x in range ( PANEL_WIDTH ) :
src_x = ( offset + x ) % w
for y in range ( PANEL_HEIGHT ) :
r , g , b = img . getpixel ( ( src_x , y ) )
canvas . SetPixel ( x , y , r , b , g ) # RBG panels: swap G and B
# ---------------------------------------------------------------------------
# Text overlay (drawn on top of the blitted PIL layer)
# ---------------------------------------------------------------------------
def _draw_overlay ( canvas , cells , scroll_x ) :
total_w = FANTASY_WIDTH * len ( cells )
for i , cell in enumerate ( cells ) :
bx = i * FANTASY_WIDTH - scroll_x
for wrap in [ 0 , total_w ] :
x = bx + wrap
if x + FANTASY_WIDTH < 0 or x > = PANEL_WIDTH :
continue
if cell [ " type " ] == " team_info " :
_draw_team_info ( canvas , cell , x )
else :
_draw_player ( canvas , cell , x )
def _draw_team_info ( canvas , cell , x ) :
"""
Layout (64 × 32 px cell):
y= 8 │ Owner display name (yellow)
y=17 │ Fantasy team name (white)
y=28 │ 144.0-134.3 score line (green if winning, red if losing)
"""
score_color = _COLOR_GREEN if cell [ " winning " ] else Colors . RED . value
graphics . DrawText ( canvas , font_small , x + 2 , 8 ,
_rbg ( Colors . YELLOW . value ) ,
cell [ " owner " ] [ : 12 ] )
graphics . DrawText ( canvas , font_small , x + 2 , 17 ,
_rbg ( Colors . WHITE . value ) ,
_team_label ( cell [ " team_name " ] ) )
score_line = f " { _fmt_pts ( cell [ ' points ' ] ) } - { _fmt_pts ( cell [ ' opp_points ' ] ) } "
graphics . DrawText ( canvas , font_small , x + 2 , 28 ,
_rbg ( score_color ) ,
score_line )
def _draw_player ( canvas , cell , x ) :
"""
Layout (64 × 32 px cell):
Left 14 px │ NFL team logo (rendered in PIL layer)
x+17, y=13 │ Player name — white / yellow (Q) / red (Out/IR)
x+17, y=26 │ " POS pts "
top-right │ injury indicator letter (Q / D / X)
"""
injury = cell . get ( " injury " )
# Name color reflects injury severity
if injury in ( " Out " , " IR " ) :
name_color = Colors . RED . value
elif injury in ( " Questionable " , " Doubtful " ) :
name_color = Colors . YELLOW . value
else :
name_color = Colors . WHITE . value
graphics . DrawText ( canvas , font_small , x + TEXT_X , 13 ,
_rbg ( name_color ) , cell [ " name " ] )
# Injury indicator letter in top-right corner of the cell
if injury :
ind_map = { " Questionable " : " Q " , " Doubtful " : " D " , " Out " : " X " , " IR " : " IR " }
indicator = ind_map . get ( injury , " ? " )
ind_color = Colors . YELLOW . value if injury in ( " Questionable " , " Doubtful " ) else Colors . RED . value
graphics . DrawText ( canvas , font_small , x + FANTASY_WIDTH - 10 , 7 ,
_rbg ( ind_color ) , indicator )
# Position + fantasy points
pts_line = f " { cell [ ' position ' ] } { _fmt_pts ( cell [ ' points ' ] ) } "
graphics . DrawText ( canvas , font_small , x + TEXT_X , 26 ,
_rbg ( Colors . WHITE . value ) , pts_line )
# ---------------------------------------------------------------------------
# Public entry point — call from the main render loop
# ---------------------------------------------------------------------------
def draw_frame ( canvas ) :
global _data , _last_fetch , _cells
global _virtual_canvas , _virtual_dirty , _scroll_x , _tick
now = time ( )
# Refresh data on interval
if now - _last_fetch > FETCH_INTERVAL or _data is None :
fresh = _fetch ( )
if fresh :
_data = fresh
_cells = _build_cells ( _data )
_virtual_dirty = True
_last_fetch = now
if not _cells :
canvas . Clear ( )
graphics . DrawText ( canvas , font , 8 , 20 ,
_rbg ( Colors . RED . value ) , " No fantasy " )
return canvas
# Rebuild PIL background layer when data changes
if _virtual_dirty or _virtual_canvas is None :
result = _build_virtual_canvas ( _cells )
if result :
_virtual_canvas , _ = result
_virtual_dirty = False
_scroll_x = 0
total_scroll_w = FANTASY_WIDTH * len ( _cells )
canvas . Clear ( )
if _virtual_canvas :
_blit_slice ( canvas , _virtual_canvas , _scroll_x )
_draw_overlay ( canvas , _cells , _scroll_x )
# Advance scroll position
_tick + = 1
if _tick > = _frames_per_tick :
_tick = 0
_scroll_x = ( _scroll_x + _scroll_speed ) % total_scroll_w
return canvas