Initial Home Assistant commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
""" 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
|
||||
Reference in New Issue
Block a user