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

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,
)