557 lines
18 KiB
Python
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
|