Files
HomeAssistance/custom_components/teamtracker/parse_espn.py
T
2026-06-05 22:34:31 -04:00

557 lines
18 KiB
Python

""" Parse ESPN JSON response """
from __future__ import annotations
from dataclasses import replace
from datetime import datetime
import logging
import re
from typing import TYPE_CHECKING
import arrow
from .const import API_LIMIT, DEFAULT_LOGO
from .models import TeamTrackerValues
from .parser_base import BaseSportParser
from .set_values import SetValuesMixin
from .utils import get_value
_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from .coordinator import TeamTrackerCoordinator
_LOGGER = logging.getLogger(__name__)
class EspnParser(BaseSportParser, SetValuesMixin):
"""Class to parse responses in ESPN JSON format."""
def __init__(self, coordinator: TeamTrackerCoordinator) -> None:
# Define the attributes that must be available on all providers
super().__init__(coordinator)
self._lang = ""
self._search_key = ""
self._stop_flag = False
self._found_competitor = False
self._event_state = "NOT_FOUND"
self._prev_values: TeamTrackerValues
def setup(self,
sensor_name: str,
sport_path: str,
league_path: str,
league_id: str,
team_id: str,
) -> bool:
self._sensor_name = sensor_name
self._sport_path = sport_path
self._league_path = league_path
self._league_id = league_id
self._default_logo = DEFAULT_LOGO
self._team_id = team_id.upper()
return True
def parse_response(
self,
provider_response,
lang: str
) -> TeamTrackerValues:
"""Loop throught the json data returned by the API to find the right event and set values"""
rc = self.initialize_sensor_values(provider_response)
if rc is False:
return self._values
data = provider_response["data"]
self._lang = lang
self._search_key = self._team_id
self._prev_values = TeamTrackerValues()
self._stop_flag = False
self._found_competitor = False
self._values.league_logo = get_value(
data, "leagues", 0, "logos", 0, "href", default=DEFAULT_LOGO
)
self._values.league_name = get_value(
data, "leagues", 0, "name", default=""
)
events = data.get("events", [])
limit_hit = len(events) == API_LIMIT
first_date = datetime(9999, 12, 31, 1, 0, 0)
last_date = datetime(1900, 1, 31, 1, 0, 0)
for event in events:
self._event_state = "NOT_FOUND"
grouping_index = -1
for grouping_index, grouping in enumerate(
get_value(event, "groupings", default=[])
):
competition_index = -1
for competition_index, competition in enumerate(
get_value(grouping, "competitions", default=[])
):
first_date, last_date = self._process_competition_dates(
event,
competition,
first_date,
last_date
)
rc = self._process_competition(
event,
grouping_index,
competition,
competition_index,
)
if not rc:
_LOGGER.debug(
"%s: parse_response() Error occurred processing competition: %s",
self._sensor_name,
self._values,
)
if self._stop_flag:
break
if grouping_index == -1:
competition_index = -1
for competition_index, competition in enumerate(
get_value(event, "competitions", default=[])
):
first_date, last_date = self._process_competition_dates(
event,
competition,
first_date,
last_date
)
rc = self._process_competition(
event,
grouping_index,
competition,
competition_index,
)
if self._stop_flag:
break
#
# if the competition state is POST but the event state is IN, stop looking
# this happens in tennis where an event has many competitions
#
if self._values.state == "POST" and self._event_state == "IN":
self._stop_flag = True
if self._stop_flag:
break
if competition_index == -1:
_LOGGER.debug(
"%s: async_process_event() No competitions for this event: %s",
self._sensor_name,
get_value(event, "shortName", default="{shortName}"),
)
if not self._found_competitor:
self._competitor_not_found(
data,
limit_hit,
first_date,
last_date,
self._team_id,
)
rc = self.finalize_sensor_values(provider_response)
return self._values
def _process_competition(self,
event,
grouping_index,
competition,
competition_index,
) -> bool:
"""Process a competition"""
competitor_index = -1
rc = True
for competitor_index, competitor in enumerate(
get_value(competition, "competitors", default=[])
):
matched_index = self._find_search_key(
event,
competition,
competitor,
competitor_index,
)
if matched_index is not None:
rc = self.process_name_match(
event,
grouping_index,
competition_index,
matched_index,
)
if not rc:
_LOGGER.debug(
"%s: async_process_competition() Error occurred processing name match: %s",
self._sensor_name,
self._values,
)
if self._stop_flag:
break
if competitor_index == -1:
_LOGGER.debug(
"%s: async_process_event() No competitors in this competition: %s",
self._sensor_name,
str(get_value(competition, "id", default="{id}")),
)
return rc
def process_name_match(self,
event,
grouping_index,
competition_index,
matched_index,
)-> bool:
"""Process a name match"""
self._found_competitor = True
self._prev_values = replace(self._values)
self._event_state = str(
get_value(
event, "status", "type", "state", default="NOT_FOUND"
)
).upper()
rc = self._set_values(
event,
grouping_index,
competition_index,
matched_index,
)
if not rc:
_LOGGER.debug(
"%s: event() Error occurred setting event values: %s",
self._sensor_name,
self._values,
)
if self._values.state == "IN":
self._stop_flag = True
time_diff = abs(
(arrow.get(self._values.date) - arrow.now()).total_seconds()
)
if self._values.state == "PRE" and time_diff < 1200:
self._stop_flag = True
if self._stop_flag:
return rc
prev_flag = self._use_prev_values_flag()
if prev_flag:
self._values = replace(self._prev_values)
return rc
#
# _async_find_search_key()
#
def _find_search_key(self,
event,
competition,
competitor,
team_index,
):
"""Check if there is a match on wildcard, team_abbreviation, event_name, or athlete_name"""
if self._search_key == "*":
_LOGGER.debug(
"%s: Found competitor using wildcard '%s'; parsing data.",
self._sensor_name,
self._search_key,
)
return team_index
if competitor["type"] == "team":
team_abbreviation = get_value(
competitor, "team", "abbreviation", default=""
)
if self._search_key == team_abbreviation:
_LOGGER.debug(
"%s: Found competition for '%s' in team abbreviation; parsing data.",
self._sensor_name,
self._search_key,
)
return team_index
team_id = str(get_value(
competitor, "team", "id", default=""
))
if self._search_key == team_id:
_LOGGER.debug(
"%s: Found competition for team '%s' in team id; parsing data.",
self._sensor_name,
self._search_key,
)
return team_index
team_name = str(get_value(
competitor, "team", "displayName", default=""
)).upper()
try:
if team_name and re.fullmatch(self._search_key, team_name):
_LOGGER.debug(
"%s: Found competition for regex '%s' in team.displayName; parsing data.",
self._sensor_name,
self._search_key,
)
return team_index
except re.error as e:
_LOGGER.warning(
"%s: Invalid regular expression '%s' in search key (exception %s)",
self._sensor_name,
self._search_key,
e,
)
return None
roster = str(get_value(
competitor, "roster", "displayName", default=""
)).upper()
try:
if roster and re.fullmatch(self._search_key, roster):
_LOGGER.debug(
"%s: Found competition for regex '%s' in roster.displayName; parsing data.",
self._sensor_name,
self._search_key,
)
return team_index
except re.error as e:
_LOGGER.warning(
"%s: Invalid regular expression '%s' in search key (exception %s)",
self._sensor_name,
self._search_key,
e,
)
return None
# Abbreviations in event_name can be different than team_abbr so look there if neither team abbrevations match
team0_abbreviation = str(
get_value(
competition, "competitors", 0, "team", "abbreviation", default=""
)
)
if team_index == 1 and self._search_key != team0_abbreviation:
event_shortname = get_value(event, "shortName", default="")
if event_shortname.startswith(self._search_key + " ") or event_shortname.endswith(
" " + self._search_key
):
self._values.api_message = (
"team_id '"
+ self._search_key
+ "' does not match team_abbr. Found in event_name."
)
_LOGGER.warning(
"%s: Found competition for '%s' in event_name; parsing data. Rebuild sensor using team_abbr for better performance.",
self._sensor_name,
self._search_key,
)
return team_index # Don't know what team to match so use this one
return None
if competitor["type"] == "athlete":
athlete_name = str(
get_value(competitor, "athlete", "displayName", default="")
).upper()
try:
if self._search_key in athlete_name or re.fullmatch(self._search_key, athlete_name):
_LOGGER.debug(
"%s: Found competition for '%s' in athlete name; parsing data",
self._sensor_name,
self._search_key,
)
return team_index
except re.error as e:
_LOGGER.warning(
"%s: Invalid regular expression '%s' in search key (exception %s)",
self._sensor_name,
self._search_key,
e,
)
return None
_LOGGER.debug(
"%s: Unexpected competitor type found '%s'",
self._sensor_name,
competitor["type"],
)
return None
#
# _async_use_prev_values_flag()
#
def _use_prev_values_flag(self):
"""Determine if prev_values should be saved"""
#
# If the state or prev_state is POST or IN and > 18 hrs in the future, treat is as PRE
# This can happen if an event is postponed
#
current_state = self._values.state
if current_state in ("POST", "IN"):
time_diff = (arrow.get(self._values.date) - arrow.now()).total_seconds()
if time_diff > 64800:
current_state = "PRE"
prev_state = self._prev_values.state
if prev_state in ("POST", "IN"):
time_diff = (arrow.get(self._prev_values.date) - arrow.now()).total_seconds()
if time_diff > 64800:
prev_state = "PRE"
if prev_state == "POST":
if current_state == "PRE":
# Use POST if PRE is more than 18 hours in future
time_diff = (arrow.get(self._values.date) - arrow.now()).total_seconds()
if time_diff > 64800:
return True
elif current_state == "POST":
# use POST w/ latest date
if arrow.get(self._prev_values.date) > arrow.get(self._values.date):
return True
if self._sport_path in ["golf", "racing"] and (
arrow.get(self._prev_values.date) == arrow.get(self._values.date)
):
return True
if prev_state == "PRE":
if current_state == "PRE":
# use PRE w/ earliest date
if arrow.get(self._prev_values.date) <= arrow.get(self._values.date):
return True
elif current_state == "POST":
# Use PRE if less than 18 hours in future
time_diff = abs(
arrow.get(self._prev_values.date) - arrow.now()
).total_seconds()
if time_diff < 64800:
return True
return False
#
# _competitor_not_found()
#
def _competitor_not_found(self,
data,
limit_hit,
first_date,
last_date,
team_id,
):
"""Handle messaging if competitor was not found"""
if limit_hit:
self._values.api_message = (
"API_LIMIT hit. No competition found for '"
+ team_id
+ "' between "
+ first_date.strftime("%Y-%m-%dT%H:%MZ")
+ " and "
+ last_date.strftime("%Y-%m-%dT%H:%MZ")
)
_LOGGER.debug(
"%s: API_LIMIT hit (%s). No competitor information '%s' returned by API",
self._sensor_name,
API_LIMIT,
self._search_key,
)
return
if self._sport_path == "racing":
events = data.get("events")
event_name = get_value(
events, 0, "shortName", default=None
)
event_date = get_value(
events, 0, "date", default=None
)
if event_name is not None:
competitors = get_value(
events, 0, "competitions", 0, "competitors", default=None
)
if competitors is None:
self._values.event_name = event_name
self._values.date = event_date
self._values.api_message = f"Drivers not found, qualifying not complete for {event_name}"
_LOGGER.debug(
"%s: No drivers found for %s",
self._sensor_name,
event_name,
)
return
self._values.api_message = (
"No competition scheduled for '"
+ team_id
+ "' between "
+ first_date.strftime("%Y-%m-%dT%H:%MZ")
+ " and "
+ last_date.strftime("%Y-%m-%dT%H:%MZ")
)
_LOGGER.debug(
"%s: No competitor information '%s' returned by API",
self._sensor_name,
self._search_key,
)
return
def _process_competition_dates(self,
event,
competition,
first_date,
last_date
) -> tuple[datetime, datetime]:
"""Process dates"""
competition_date_str = get_value(
competition, "date", default=(get_value(event, "date"))
)
try:
competition_date = datetime.fromisoformat(
str(competition_date_str).replace("Z", "+00:00")
).replace(tzinfo=None)
last_date = max(last_date, competition_date)
first_date = min(first_date, competition_date)
except (ValueError, TypeError):
pass
return first_date, last_date