496 lines
18 KiB
Python
496 lines
18 KiB
Python
"""Adds config flow for TeamTracker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.const import CONF_NAME
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
from .const import (
|
|
CONF_API_LANGUAGE,
|
|
CONF_CONFERENCE_ID,
|
|
CONF_LEAGUE_ID,
|
|
CONF_LEAGUE_PATH,
|
|
CONF_SPORT_PATH,
|
|
CONF_TEAM_ID,
|
|
DOMAIN,
|
|
INDIVIDUAL_SPORTS,
|
|
NATIVE_LEAGUES,
|
|
)
|
|
from .provider_base import BaseSportProvider
|
|
from .provider_factory import get_provider
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
# Sport groups: key → (display_name, {league_id: display_label})
|
|
_SPORT_GROUPS: dict[str, tuple[str, dict[str, str]]] = {
|
|
"australian-football": ("Australian Football", {
|
|
"AFL": "AFL",
|
|
}),
|
|
"baseball": ("Baseball", {
|
|
"MLB": "MLB",
|
|
}),
|
|
"basketball": ("Basketball", {
|
|
"NBA": "NBA",
|
|
"NCAAM": "NCAA Men's Basketball",
|
|
"NCAAW": "NCAA Women's Basketball",
|
|
"WNBA": "WNBA",
|
|
}),
|
|
"football": ("Football", {
|
|
"NCAAF": "NCAA Football",
|
|
"NFL": "NFL",
|
|
"XFL": "XFL",
|
|
}),
|
|
"golf": ("Golf", {
|
|
"PGA": "PGA Tour",
|
|
}),
|
|
"hockey": ("Hockey", {
|
|
"NHL": "NHL",
|
|
}),
|
|
"mma": ("MMA", {
|
|
"UFC": "UFC",
|
|
}),
|
|
"racing": ("Racing", {
|
|
"F1": "Formula 1",
|
|
"IRL": "IndyCar",
|
|
"NASCAR": "NASCAR Cup Series",
|
|
}),
|
|
"soccer-us": ("Soccer (U.S.)", {
|
|
"MLS": "MLS",
|
|
"NWSL": "NWSL",
|
|
}),
|
|
"soccer-intl": ("Soccer (International)", {
|
|
"BUND": "Bundesliga",
|
|
"CL": "Champions League",
|
|
"CLA": "Copa Libertadores",
|
|
"EPL": "Premier League",
|
|
"LIGA": "La Liga",
|
|
"LIG1": "Ligue 1",
|
|
"SERA": "Serie A",
|
|
"WC": "World Cup",
|
|
"WWC": "Women's World Cup",
|
|
}),
|
|
"tennis": ("Tennis", {
|
|
"ATP": "ATP",
|
|
"WTA": "WTA",
|
|
}),
|
|
"volleyball": ("Volleyball", {
|
|
"NCAAVB": "NCAA Men's Volleyball",
|
|
"NCAAVBW": "NCAA Women's Volleyball",
|
|
}),
|
|
}
|
|
|
|
SPORT_OPTIONS: dict[str, str] = {
|
|
"XXX": "Custom API",
|
|
**{k: v[0] for k, v in _SPORT_GROUPS.items()}
|
|
}
|
|
|
|
|
|
def _get_path_schema(
|
|
user_input: dict[str, Any] | None,
|
|
default_dict: dict[str, Any],
|
|
) -> vol.Schema:
|
|
"""Schema for custom sport/league path step."""
|
|
if user_input is None:
|
|
user_input = {}
|
|
|
|
def _get_default(key: str) -> Any:
|
|
return user_input.get(key, default_dict.get(key, ""))
|
|
|
|
return vol.Schema(
|
|
{
|
|
vol.Required(CONF_SPORT_PATH, default=_get_default(CONF_SPORT_PATH)): str,
|
|
vol.Required(CONF_LEAGUE_PATH, default=_get_default(CONF_LEAGUE_PATH)): str,
|
|
vol.Required(CONF_TEAM_ID, default=_get_default(CONF_TEAM_ID)): cv.string,
|
|
vol.Optional(CONF_CONFERENCE_ID, default=_get_default(CONF_CONFERENCE_ID)): cv.string,
|
|
vol.Optional(CONF_NAME, default=_get_default(CONF_NAME)): cv.string,
|
|
}
|
|
)
|
|
|
|
|
|
class TeamTrackerScoresFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
|
|
"""Config flow for TeamTracker."""
|
|
|
|
VERSION = 3
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize."""
|
|
self._sport_key: str = ""
|
|
self._league_id: str = ""
|
|
self._team_name: str = ""
|
|
self._sport_path: str = ""
|
|
self._league_path: str = ""
|
|
self._all_teams: list[dict] = []
|
|
self._search_results: dict[str, str] = {}
|
|
self._team_meta: dict[str, dict] = {}
|
|
self._errors: dict[str, str] = {}
|
|
self._entry_data: dict[str, Any] = {}
|
|
self._provider: BaseSportProvider | None= None
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 1: choose sport group #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle a flow initialized by the user."""
|
|
self._errors = {}
|
|
|
|
if user_input is not None:
|
|
sport_key = user_input["sport_key"]
|
|
if sport_key == "XXX":
|
|
return await self.async_step_custom_api()
|
|
self._sport_key = sport_key
|
|
leagues = _SPORT_GROUPS[sport_key][1]
|
|
if len(leagues) == 1:
|
|
# Only one league for this sport — skip league step
|
|
self._league_id = next(iter(leagues))
|
|
self._sport_path = NATIVE_LEAGUES.get(self._league_id, {}).get(CONF_SPORT_PATH, "")
|
|
self._league_path = NATIVE_LEAGUES.get(self._league_id, {}).get(CONF_LEAGUE_PATH, "")
|
|
|
|
return await self.async_step_search()
|
|
return await self.async_step_league()
|
|
|
|
schema = vol.Schema(
|
|
{vol.Required("sport_key"): vol.In(SPORT_OPTIONS)}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=schema,
|
|
errors=self._errors,
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 2a: Set Up Custom API (sport_key = XXX) #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_custom_api(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle custom sport/league path configuration."""
|
|
self._errors = {}
|
|
|
|
if user_input is not None:
|
|
self._league_id = "XXX"
|
|
self._sport_path = user_input[CONF_SPORT_PATH]
|
|
self._league_path = user_input[CONF_LEAGUE_PATH]
|
|
return await self.async_step_search()
|
|
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_SPORT_PATH, default=""): cv.string,
|
|
vol.Required(CONF_LEAGUE_PATH, default=""): cv.string,
|
|
}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="custom_api",
|
|
data_schema=schema,
|
|
errors=self._errors,
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 2b: choose league within sport #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_league(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle league selection within the chosen sport."""
|
|
self._errors = {}
|
|
|
|
if user_input is not None:
|
|
self._league_id = user_input[CONF_LEAGUE_ID]
|
|
self._sport_path = NATIVE_LEAGUES.get(self._league_id, {}).get(CONF_SPORT_PATH, "")
|
|
self._league_path = NATIVE_LEAGUES.get(self._league_id, {}).get(CONF_LEAGUE_PATH, "")
|
|
|
|
return await self.async_step_search()
|
|
|
|
league_options = _SPORT_GROUPS[self._sport_key][1]
|
|
sport_name = _SPORT_GROUPS[self._sport_key][0]
|
|
schema = vol.Schema(
|
|
{vol.Required(CONF_LEAGUE_ID): vol.In(league_options)}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="league",
|
|
data_schema=schema,
|
|
errors=self._errors,
|
|
description_placeholders={"sport_name": sport_name},
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 3: search team (ESPN link always correct here) #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_search(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle team search step."""
|
|
self._errors = {}
|
|
|
|
# Individual sports (golf, mma, tennis) have athletes, not teams —
|
|
# the ESPN teams API returns nothing useful, so skip straight to manual.
|
|
if user_input is None and self._sport_path in INDIVIDUAL_SPORTS:
|
|
return await self.async_step_manual_athlete(user_input=None)
|
|
|
|
if user_input is not None:
|
|
self._provider = get_provider(self._sport_path, self._league_path)
|
|
search_term = user_input.get("search_team", "").strip().lower()
|
|
if search_term:
|
|
response = await self._provider.async_fetch_team_data(self.hass, self._sport_path, self._league_path)
|
|
self._all_teams = response["data"]
|
|
if not self._all_teams:
|
|
self._errors["base"] = "cannot_fetch_teams"
|
|
else:
|
|
filtered = [
|
|
t for t in self._all_teams
|
|
if search_term in t["displayName"].lower()
|
|
or search_term in t["abbreviation"].lower()
|
|
or search_term in t["location"].lower()
|
|
or search_term in t["id"]
|
|
]
|
|
if not filtered:
|
|
self._errors["search_team"] = "no_teams_found"
|
|
else:
|
|
self._search_results = {
|
|
t["id"]: f"{t['displayName']} ({t['abbreviation']} - {t['id']})"
|
|
for t in filtered
|
|
}
|
|
self._team_meta = {t["id"]: t for t in filtered}
|
|
return await self.async_step_select_team()
|
|
else:
|
|
return await self.async_step_manual_team()
|
|
|
|
schema = vol.Schema(
|
|
{vol.Optional("search_team", default=""): str}
|
|
)
|
|
sport_name = _SPORT_GROUPS.get(self._sport_key, ("",))[0]
|
|
league_name = _SPORT_GROUPS.get(self._sport_key, ("", {}))[1].get(self._league_id, "")
|
|
return self.async_show_form(
|
|
step_id="search",
|
|
data_schema=schema,
|
|
errors=self._errors,
|
|
description_placeholders={
|
|
"league_id": self._league_id,
|
|
"league_name": league_name,
|
|
"sport_name": sport_name,
|
|
},
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 4a: pick from search results #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_select_team(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle team selection from search results."""
|
|
|
|
if user_input is not None:
|
|
t_id = user_input["team_selection"]
|
|
meta = self._team_meta.get(t_id, {})
|
|
|
|
self._team_name = meta.get("displayName", t_id)
|
|
name = user_input.get(CONF_NAME, "").strip() or meta.get("displayName", t_id)
|
|
team_id = meta.get("id", t_id)
|
|
|
|
self._entry_data = {
|
|
CONF_NAME: name,
|
|
CONF_LEAGUE_ID: self._league_id,
|
|
CONF_TEAM_ID: team_id,
|
|
CONF_SPORT_PATH: self._sport_path,
|
|
CONF_LEAGUE_PATH: self._league_path,
|
|
}
|
|
if "college" in self._league_path and self._provider:
|
|
conf_id = await self._provider.async_fetch_team_conference_id(self.hass, self._sport_path, self._league_path, team_id)
|
|
self._entry_data[CONF_CONFERENCE_ID] = conf_id
|
|
|
|
return await self.async_step_finalize()
|
|
|
|
sport_name = _SPORT_GROUPS.get(self._sport_key, ("",))[0]
|
|
league_name = _SPORT_GROUPS.get(self._sport_key, ("", {}))[1].get(self._league_id, "")
|
|
schema = vol.Schema({
|
|
vol.Required("team_selection"): vol.In(self._search_results),
|
|
})
|
|
return self.async_show_form(
|
|
step_id="select_team",
|
|
data_schema=schema,
|
|
errors={},
|
|
description_placeholders={
|
|
"league_id": self._league_id,
|
|
"sport_name": sport_name,
|
|
"league_name": league_name,
|
|
},
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 4b: manual team_id entry (no search / fallback) #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_manual_team(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle manual team ID entry."""
|
|
|
|
if user_input is not None:
|
|
sport_path = self._sport_path
|
|
league_path = self._league_path
|
|
self._team_name = user_input[CONF_TEAM_ID]
|
|
name = user_input.get(CONF_NAME) or user_input[CONF_TEAM_ID]
|
|
team_id = user_input[CONF_TEAM_ID]
|
|
self._entry_data = {
|
|
CONF_NAME: name,
|
|
CONF_LEAGUE_ID: self._league_id,
|
|
CONF_TEAM_ID: team_id,
|
|
CONF_SPORT_PATH: sport_path,
|
|
CONF_LEAGUE_PATH: league_path,
|
|
}
|
|
if "college" in league_path and self._provider:
|
|
conf_id = await self._provider.async_fetch_team_conference_id(self.hass, sport_path, league_path, team_id)
|
|
self._entry_data[CONF_CONFERENCE_ID] = conf_id
|
|
|
|
return await self.async_step_finalize()
|
|
|
|
sport_name = _SPORT_GROUPS.get(self._sport_key, ("",))[0]
|
|
league_name = _SPORT_GROUPS.get(self._sport_key, ("", {}))[1].get(self._league_id, "")
|
|
|
|
schema_dict = {
|
|
vol.Required(CONF_TEAM_ID): cv.string,
|
|
}
|
|
|
|
return self.async_show_form(
|
|
step_id="manual_team",
|
|
data_schema=vol.Schema(schema_dict),
|
|
errors={},
|
|
description_placeholders={
|
|
"league_id": self._league_id,
|
|
"sport_name": sport_name,
|
|
"league_name": league_name,
|
|
},
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 4c: manual athlete entry (no search / fallback) #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_manual_athlete(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Handle manual team ID entry."""
|
|
if user_input is not None:
|
|
name = user_input.get(CONF_NAME) or user_input[CONF_TEAM_ID]
|
|
self._team_name = user_input[CONF_TEAM_ID]
|
|
self._entry_data = {
|
|
CONF_NAME: name,
|
|
CONF_LEAGUE_ID: self._league_id,
|
|
CONF_TEAM_ID: user_input[CONF_TEAM_ID],
|
|
CONF_SPORT_PATH: self._sport_path,
|
|
CONF_LEAGUE_PATH: self._league_path,
|
|
}
|
|
|
|
return await self.async_step_finalize()
|
|
|
|
sport_name = _SPORT_GROUPS.get(self._sport_key, ("",))[0]
|
|
league_name = _SPORT_GROUPS.get(self._sport_key, ("", {}))[1].get(self._league_id, "")
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_TEAM_ID): cv.string,
|
|
}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="manual_athlete",
|
|
data_schema=schema,
|
|
errors={},
|
|
description_placeholders={
|
|
"league_id": self._league_id,
|
|
"sport_name": sport_name,
|
|
"league_name": league_name,
|
|
},
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 5: Finalize the configuration and choose a name #
|
|
# ------------------------------------------------------------------ #
|
|
async def async_step_finalize(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Step 5: Finalize the configuration and choose a name."""
|
|
if user_input is not None:
|
|
name = user_input[CONF_NAME]
|
|
self._entry_data[CONF_NAME] = name
|
|
|
|
return self.async_create_entry(
|
|
title=name,
|
|
data=self._entry_data,
|
|
)
|
|
|
|
default_name = f"{self._league_id} - {self._team_name}"
|
|
# Use the league_id and team_name as the default name
|
|
schema = vol.Schema({
|
|
vol.Required(CONF_NAME, default=default_name): cv.string,
|
|
})
|
|
|
|
return self.async_show_form(
|
|
step_id="finalize",
|
|
data_schema=schema,
|
|
description_placeholders={
|
|
"team_name": self._team_name,
|
|
"league_name": self._league_id,
|
|
},
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Options flow (reconfigure existing entry) #
|
|
# ------------------------------------------------------------------ #
|
|
@staticmethod
|
|
@callback
|
|
def async_get_options_flow(
|
|
config_entry: config_entries.ConfigEntry,
|
|
) -> config_entries.OptionsFlow:
|
|
"""Get the options flow for this handler."""
|
|
return TeamTrackerScoresOptionsFlow(config_entry)
|
|
|
|
|
|
class TeamTrackerScoresOptionsFlow(config_entries.OptionsFlow):
|
|
"""Options flow for TeamTracker."""
|
|
|
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
|
"""Initialize."""
|
|
self.entry = config_entry
|
|
self._options: dict[str, Any] = dict(config_entry.options)
|
|
self._errors: dict[str, str] = {}
|
|
|
|
async def async_step_init(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> config_entries.FlowResult:
|
|
"""Manage options."""
|
|
if user_input is not None:
|
|
self._options.update(user_input)
|
|
return self.async_create_entry(title="", data=self._options)
|
|
|
|
lang = None
|
|
if (
|
|
self.entry
|
|
and self.entry.options
|
|
and CONF_API_LANGUAGE in self.entry.options
|
|
):
|
|
lang = self.entry.options[CONF_API_LANGUAGE]
|
|
|
|
options_schema = vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_API_LANGUAGE,
|
|
description={"suggested_value": lang},
|
|
default="",
|
|
): cv.string,
|
|
}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="init",
|
|
data_schema=options_schema,
|
|
errors=self._errors,
|
|
)
|