339 lines
12 KiB
Python
339 lines
12 KiB
Python
""" Parse CFL Scoreboard JSON response """
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
|
|
import arrow
|
|
|
|
from .const import DEFAULT_LOGO
|
|
from .models import TeamTrackerValues
|
|
from .parser_base import BaseSportParser
|
|
from .utils import get_value
|
|
|
|
if TYPE_CHECKING:
|
|
from .coordinator import TeamTrackerCoordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
DEFAULT_COLORS = ["#D3D3D3", "#A9A9A9"]
|
|
|
|
class CflScoreboardParser(BaseSportParser):
|
|
"""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
|
|
|
|
self._team_side = ""
|
|
self._opponent_side = ""
|
|
|
|
|
|
#
|
|
# initialize_values()
|
|
# Set sensor attributes that do not rely on the API
|
|
#
|
|
def initialize_sensor_values(self, provider_response) -> bool:
|
|
rc = super().initialize_sensor_values(provider_response)
|
|
self._values.sport = "football"
|
|
|
|
return rc
|
|
|
|
|
|
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
|
|
|
|
weekly_schedule = self._get_current_schedule(data)
|
|
week_name = get_value(weekly_schedule, "name", default="")
|
|
first_date_str = get_value(weekly_schedule, "startDate", default="")
|
|
last_date_str = get_value(weekly_schedule, "endDate", default="")
|
|
|
|
tournaments = get_value(weekly_schedule, "tournaments", default=[])
|
|
|
|
tournament = self._get_tournament(tournaments, self._team_id)
|
|
|
|
if tournament:
|
|
rc = self._set_values(weekly_schedule, tournament)
|
|
if rc is False:
|
|
_LOGGER.debug(
|
|
"%s: Error parsing response for '%s' for CFL '%s'",
|
|
self._sensor_name,
|
|
self._search_key,
|
|
week_name
|
|
)
|
|
else:
|
|
first_date = datetime.fromisoformat(str(first_date_str)).replace(tzinfo=None)
|
|
last_date = datetime.fromisoformat(str(last_date_str)).replace(tzinfo=None)
|
|
|
|
self._values.api_message = (
|
|
"No competition scheduled for '"
|
|
+ str(self._values.team_abbr)
|
|
+ "' in CFL '"
|
|
+ week_name
|
|
+ "' 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 for %s",
|
|
self._sensor_name,
|
|
self._search_key,
|
|
week_name
|
|
)
|
|
|
|
rc = self.finalize_sensor_values(provider_response)
|
|
|
|
return self._values
|
|
|
|
|
|
|
|
#
|
|
# _get_current_schedule()
|
|
#
|
|
def _get_current_schedule(self, rounds) -> dict:
|
|
"""Return the tournaments for the current active or recently completed round."""
|
|
if not rounds:
|
|
return {}
|
|
|
|
# Get the current time in ISO format to match the API's timezone-aware strings
|
|
# The API uses '+00:00', which aligns with UTC
|
|
now = datetime.utcnow()
|
|
|
|
# Calculate the cutoff timestamp (24 hours ago) in ISO format
|
|
cutoff_time = now - timedelta(hours=24)
|
|
cutoff_iso = cutoff_time.isoformat() + "+00:00"
|
|
|
|
r = {}
|
|
for r in rounds:
|
|
status = r.get("status", "").lower()
|
|
end_date = r.get("endDate", "")
|
|
|
|
# 1. Condition: The round is explicitly not complete (e.g., 'playing' or 'scheduled')
|
|
if status != "complete":
|
|
return r
|
|
|
|
# 2. Condition: The round is complete, but it ended within the last 24 hours
|
|
if end_date and end_date >= cutoff_iso:
|
|
return r
|
|
|
|
# 3. Fallback Condition: If all rounds are complete and past the 24h window,
|
|
# return the very last round item in the list so the sensor doesn't go blank.
|
|
return r[-1]
|
|
|
|
|
|
#
|
|
# _get_tournament()
|
|
#
|
|
def _get_tournament(self,
|
|
tournaments,
|
|
search_key,
|
|
) -> dict:
|
|
"""Check if there is a match on wildcard, team_abbreviation, event_name, or athlete_name"""
|
|
|
|
for t in tournaments:
|
|
for side in ("home", "away"):
|
|
self._team_side = side
|
|
self._opponent_side = "away" if side == "home" else "home"
|
|
|
|
if search_key == "*":
|
|
_LOGGER.debug(
|
|
"%s: Found competitor using wildcard '%s'; parsing data.",
|
|
self._sensor_name,
|
|
search_key,
|
|
)
|
|
return t
|
|
|
|
team_id = str(get_value(
|
|
t, f"{side}Squad", "id", default=""
|
|
))
|
|
if search_key == team_id:
|
|
_LOGGER.debug(
|
|
"%s: Found competition for '%s' in team id; parsing data.",
|
|
self._sensor_name,
|
|
search_key,
|
|
)
|
|
return t
|
|
|
|
team_abbr = get_value(
|
|
t, f"{side}Squad", "shortName", default=""
|
|
)
|
|
if search_key == team_abbr:
|
|
_LOGGER.debug(
|
|
"%s: Found competition for '%s' in team shortName; parsing data.",
|
|
self._sensor_name,
|
|
search_key,
|
|
)
|
|
return t
|
|
|
|
team_name = str(get_value(
|
|
t, f"{side}Squad", "name", default=""
|
|
)).upper()
|
|
|
|
try:
|
|
if team_name and re.fullmatch(search_key, team_name):
|
|
_LOGGER.debug(
|
|
"%s: Found competition for regex '%s' in team name; parsing data.",
|
|
self._sensor_name,
|
|
search_key,
|
|
)
|
|
return t
|
|
except re.error as e:
|
|
_LOGGER.warning(
|
|
"%s: Invalid regular expression '%s' in search key (exception %s)",
|
|
self._sensor_name,
|
|
search_key,
|
|
e,
|
|
)
|
|
return {}
|
|
|
|
return {}
|
|
|
|
|
|
#
|
|
# Set Values
|
|
#
|
|
def _set_values(
|
|
self,
|
|
schedule,
|
|
tournament
|
|
) -> bool:
|
|
|
|
status = get_value(tournament, "status", default="")
|
|
if status.lower() == "complete":
|
|
self._values.state = "POST"
|
|
elif status.lower() == "scheduled":
|
|
self._values.state = "PRE"
|
|
else:
|
|
self._values.state = "IN"
|
|
|
|
self._values.season = get_value(schedule, "type", default="")
|
|
|
|
# Event Details
|
|
self._values.team_abbr = get_value(tournament, f"{self._team_side}Squad", "shortName", default="")
|
|
self._values.opponent_abbr = get_value(tournament, f"{self._opponent_side}Squad", "shortName", default="")
|
|
away = get_value(tournament, "awaySquad", "shortName", default="{shortName}")
|
|
home = get_value(tournament, "homeSquad", "shortName", default="{shortName}")
|
|
self._values.event_name = f"{away}@{home}"
|
|
self._values.event_id = get_value(tournament, "cflId", default=None)
|
|
self._values.event_id = None if (self._values.event_id is None) else str(self._values.event_id)
|
|
self._values.date = get_value(tournament, "date")
|
|
self._values.kickoff_in = arrow.get(self._values.date).humanize(locale=self._lang)
|
|
self._values.series_summary = None
|
|
self._values.venue = None
|
|
self._values.location = None
|
|
self._values.tv_network = None
|
|
odds = get_value(tournament, "markets", "away", "value", default="")
|
|
self._values.odds = f"{self._values.team_abbr} {odds}"
|
|
self._values.overunder = None
|
|
|
|
# Team Data
|
|
self._values.team_name = get_value(tournament, f"{self._team_side}Squad", "name", default="")
|
|
self._values.team_long_name = self._values.team_name
|
|
self._values.team_id = str(get_value(tournament, f"{self._team_side}Squad", "id", default=""))
|
|
self._values.team_record = None
|
|
self._values.team_rank = None
|
|
self._values.team_conference_id = None
|
|
self._values.team_homeaway = self._team_side
|
|
self._values.team_logo = None
|
|
self._values.team_url = None
|
|
self._values.team_colors = DEFAULT_COLORS
|
|
self._values.team_score = get_value(tournament, f"{self._team_side}Squad", "score")
|
|
self._values.team_win_probability = None
|
|
winner = str(get_value(tournament, "winner", default=""))
|
|
self._values.team_winner = (winner == self._values.team_id)
|
|
self._values.team_timeouts = get_value(tournament, "timeouts", f"{self._team_side}")
|
|
|
|
# Opponent Data
|
|
self._values.opponent_name = get_value(tournament, f"{self._opponent_side}Squad", "name", default="")
|
|
self._values.opponent_long_name = self._values.opponent_name
|
|
self._values.opponent_id = str(get_value(tournament, f"{self._opponent_side}Squad", "id", default=""))
|
|
self._values.opponent_record = None
|
|
self._values.opponent_rank = None
|
|
self._values.opponent_conference_id = None
|
|
self._values.opponent_homeaway = self._opponent_side
|
|
self._values.opponent_logo = None
|
|
self._values.opponent_url = None
|
|
self._values.opponent_colors = DEFAULT_COLORS
|
|
self._values.opponent_score = get_value(tournament, f"{self._opponent_side}Squad", "score")
|
|
self._values.team_win_probability = None
|
|
winner = str(get_value(tournament, "winner", default=""))
|
|
self._values.opponent_winner = (winner == self._values.opponent_id)
|
|
self._values.opponent_timeouts = get_value(tournament, "timeouts", f"{self._opponent_side}")
|
|
|
|
# In Game Attributes
|
|
self._values.quarter = get_value(tournament, "activePeriod")
|
|
self._values.clock = get_value(tournament, "clock")
|
|
possession = str(get_value(tournament, "possession", "")).lower()
|
|
if possession == self._team_side:
|
|
self._values.possession = self._values.team_id
|
|
elif possession == self._opponent_side:
|
|
self._values.possession = self._values.opponent_id
|
|
else:
|
|
self._values.possession = None
|
|
self._values.last_play = None
|
|
self._values.down_distance_text = None
|
|
|
|
# Baseball Specific
|
|
self._values.outs = None
|
|
self._values.balls = None
|
|
self._values.strikes = None
|
|
self._values.on_first = None
|
|
self._values.on_second = None
|
|
self._values.on_third = None
|
|
|
|
# Soccer/Hockey
|
|
self._values.team_shots_on_target = None
|
|
self._values.team_total_shots = None
|
|
self._values.opponent_shots_on_target = None
|
|
self._values.opponent_total_shots = None
|
|
|
|
# Volleyball
|
|
self._values.team_sets_won = None
|
|
self._values.opponent_sets_won = None
|
|
|
|
# System/API Metadata
|
|
if self._values.state == "IN":
|
|
self._values.private_fast_refresh = True
|
|
|
|
return True
|