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