215 lines
7.9 KiB
Python
215 lines
7.9 KiB
Python
""" Provide response from ESPN APIs for league_path = all & team_id is an integer """
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
|
import logging
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from .const import API_LIMIT
|
|
from .provide_espn import EspnProvider
|
|
from .utils import has_team, season_slug_to_name
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
if TYPE_CHECKING:
|
|
from .coordinator import TeamTrackerCoordinator
|
|
|
|
DATA_PROVIDER_ESPN_ALL_LEAGUES = "espn-all_leagues"
|
|
ESPNALL_DATA_FORMAT = "espnall-json"
|
|
ESPN_BASE_URL = "https://site.api.espn.com/apis/site/v2/sports"
|
|
|
|
|
|
class EspnAllLeaguesProvider(EspnProvider):
|
|
"""Provider for ESPN data when league_path is all and team_id is an integer."""
|
|
|
|
|
|
#
|
|
# __init__()
|
|
# Reuse EspnProvider settings except:
|
|
# - DATA_PROVIDER
|
|
# - async_fetch_scoreboard_data()
|
|
#
|
|
def __init__(self, coordinator: TeamTrackerCoordinator | None = None) -> None:
|
|
super().__init__(coordinator)
|
|
self.DATA_PROVIDER: str = DATA_PROVIDER_ESPN_ALL_LEAGUES
|
|
self.TEAM_SCHEDULE_KEY: str = "team-schedule-key"
|
|
self.data_format = ESPNALL_DATA_FORMAT
|
|
self.lookups: dict[str, list] = {}
|
|
self.instance_cache: dict[str, dict] = {}
|
|
|
|
|
|
#
|
|
# _get_cache_key()
|
|
# Return unique key for espn all calls
|
|
#
|
|
def _get_cache_key(self) -> str:
|
|
"""Return cache key"""
|
|
|
|
if not self._coordinator:
|
|
return ""
|
|
|
|
sport_path = self._coordinator.sport_path
|
|
league_path = self._coordinator.league_path
|
|
conference_id = self._coordinator.conference_id
|
|
team_id = self._coordinator.team_id
|
|
|
|
lang = self._coordinator.get_lang()
|
|
|
|
# For "all" leagues, include team_id in cache key since each team
|
|
# uses different narrow date windows for the scoreboard call.
|
|
key = self.DATA_PROVIDER + ":" + sport_path + ":" + league_path + ":" + conference_id + ":" + lang + ":" + team_id
|
|
|
|
return key
|
|
|
|
|
|
#
|
|
# async_fetch_scoreboard_data()
|
|
# ESPN APIs returning all leagues quickly hit the API_LIMIT, so force use of tight date ranges
|
|
# 1. Get the team schedule from ESPN and determine next upcoming game
|
|
# 2. Call w/ date range up to upcoming game
|
|
# 2. Call w/ date range around upcoming game
|
|
#
|
|
async def async_fetch_scoreboard_data(
|
|
self,
|
|
hass: HomeAssistant,
|
|
lang: str,
|
|
) -> dict:
|
|
"""Gets data from ESPN APIs for all leagues in specified sport."""
|
|
|
|
if not self._coordinator:
|
|
return{"data": None, "url": None}
|
|
|
|
sensor_name = self._coordinator.name
|
|
sport_path = self._coordinator.sport_path
|
|
league_path = self._coordinator.league_path
|
|
team_id = self._coordinator.team_id.upper()
|
|
|
|
# Get date of next game
|
|
schedule_info = await self._async_get_team_schedule()
|
|
next_game_date = schedule_info.get("next_game_date") if schedule_info else None
|
|
|
|
# Narrow window: cover recent results and upcoming game if within 7 days
|
|
today_utc = datetime.now(timezone.utc).date()
|
|
day_before_yesterday = today_utc - timedelta(days=2)
|
|
|
|
d1 = day_before_yesterday.strftime("%Y%m%d")
|
|
if next_game_date and next_game_date <= today_utc + timedelta(days=7):
|
|
d2 = next_game_date.strftime("%Y%m%d")
|
|
else:
|
|
d2 = today_utc.strftime("%Y%m%d")
|
|
|
|
_LOGGER.debug(
|
|
"%s: All-league scoreboard call 1/1 dates=%s-%s (next_game=%s)",
|
|
sensor_name, d1, d2,
|
|
next_game_date.isoformat() if next_game_date else "unknown",
|
|
)
|
|
|
|
url_parms = {}
|
|
url_parms["lang"] = lang[:2]
|
|
url_parms["limit"] = str(API_LIMIT)
|
|
url_parms["dates"] = f"{d1}-{d2}"
|
|
|
|
url = f"{ESPN_BASE_URL}/{sport_path}/{league_path}/scoreboard"
|
|
|
|
response = await self.async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
|
|
data = response["data"]
|
|
|
|
# If event for team not returned, narrow date range and try again
|
|
if has_team(data, team_id) is False:
|
|
if (next_game_date and next_game_date > today_utc):
|
|
nd1 = (next_game_date - timedelta(days=1)).strftime("%Y%m%d")
|
|
nd2 = next_game_date.strftime("%Y%m%d")
|
|
if nd1 != d1 or nd2 != d2: # avoid duplicate call
|
|
_LOGGER.debug(
|
|
"%s: All-league scoreboard call 2/2 dates=%s-%s (fallback to next game)",
|
|
sensor_name, nd1, nd2,
|
|
)
|
|
|
|
url_parms["dates"] = f"{nd1}-{nd2}"
|
|
url = f"{ESPN_BASE_URL}/{sport_path}/{league_path}/scoreboard"
|
|
|
|
response = await self.async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
|
|
|
|
# Add required lookup tables
|
|
if "team_list" not in self.lookups:
|
|
teams_response = await self.async_fetch_team_data(hass, sport_path, league_path, sensor_name)
|
|
teams_data = teams_response["data"]
|
|
self.lookups["team_list"] = teams_data
|
|
response["lookups"] = self.lookups
|
|
|
|
|
|
return response
|
|
|
|
|
|
#
|
|
# _async_get_team_schedule()
|
|
#
|
|
# Calls the team info and schedule endpoints to discover the next game
|
|
# date and build an event_id → league name mapping (substring of season)
|
|
# Results are cached in the instance_cache until the next game date passes.
|
|
#
|
|
async def _async_get_team_schedule(self):
|
|
"""Fetch team schedule info for 'all' league date computation."""
|
|
|
|
team_id = self._coordinator.team_id
|
|
sport_path = self._coordinator.sport_path
|
|
league_path = self._coordinator.league_path
|
|
sensor_name = self._coordinator.name
|
|
|
|
today = date.today()
|
|
cache = self.instance_cache.get(self.TEAM_SCHEDULE_KEY)
|
|
|
|
if cache is not None and today <= cache["expires"]:
|
|
_LOGGER.debug("%s: instance_cache hit for '%s'", sensor_name, team_id)
|
|
self.lookups["derived_league_name"] = cache["derived_league_name"]
|
|
return cache
|
|
|
|
team_url = f"{ESPN_BASE_URL}/{sport_path}/{league_path}/teams/{team_id}"
|
|
|
|
next_events = []
|
|
|
|
response = await self.async_call_espn_api(self._coordinator.hass, team_url, None, sensor_name, team_id)
|
|
team_data = response["data"]
|
|
|
|
# Try to derive the league_name from the season name or slug
|
|
# since not available from scoreboard API w/ league = "all"
|
|
season_name = ""
|
|
if team_data:
|
|
next_events = team_data.get("team", {}).get("nextEvent", [])
|
|
for ne in next_events:
|
|
eid = ne.get("id")
|
|
if not eid:
|
|
continue
|
|
season_name = ne.get("season", {}).get("displayName") or season_slug_to_name(
|
|
ne.get("season", {}).get("slug", "")
|
|
)
|
|
|
|
schedule_url = team_url + "/schedule"
|
|
response = await self.async_call_espn_api(self._coordinator.hass, schedule_url, None, sensor_name, team_id)
|
|
sched_data = response["data"]
|
|
if sched_data:
|
|
for e in sched_data.get("events", []):
|
|
eid = e.get("id")
|
|
if not eid:
|
|
continue
|
|
season_name = e.get("season", {}).get("displayName") or season_slug_to_name(
|
|
e.get("season", {}).get("slug", "")
|
|
)
|
|
|
|
derived_league_name = re.sub(r"^\d{4}(-\d{2})?\s+", "", season_name)
|
|
|
|
self.lookups["derived_league_name"] = derived_league_name
|
|
next_game_date = (
|
|
date.fromisoformat(next_events[0]["date"][:10]) if next_events else None
|
|
)
|
|
|
|
result = {
|
|
"next_game_date": next_game_date,
|
|
"derived_league_name": derived_league_name,
|
|
"expires": next_game_date or today,
|
|
}
|
|
self.instance_cache[self.TEAM_SCHEDULE_KEY] = result
|
|
return result |