Skip to content

Commit 0974f99

Browse files
authored
Hockeytech enhancements - For Pre-Release Only (#318)
HockeyTech Enhancements and Bug Fixes - Expand to all leagues supported by the HockeyTech API - api_url updated to show all parms used - Enhance testing for hockeytech API (PRE, POST, IN states) - Change attribution to HockeyTech from ESPN - Subit preferred language to HockeyTech API - Reduce refresh rate during IN state for HockeyTech to 60 seconds due to low update rate - Populate league_name for HockeyTech sensors - Update ESPN api's to HockeyTech model for consistency To Do - Multiple HockeyTech attributes have Invalid URLs - Hardcoded URLs do not extend beyond PWHL - Improve Config Flow for HockeyTech APIs - Add copyright notice to HockeyTech attribution
1 parent 1b78a51 commit 0974f99

14 files changed

Lines changed: 761 additions & 223 deletions

File tree

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,24 @@ The [Custom APIs](https://github.com/vasqued2/ha-teamtracker?tab=readme-ov-file#
5454

5555
#### Sports / Leagues Supported by non-ESPN APIs
5656

57-
Custom APIs can be configured for the following leagues using non-ESPN APIs
58-
59-
| sport_path | league_path | League | Teams | API Provider |
60-
| --- | --- | --- | --- | --- |
61-
| hockeytech | PWHL | [Professional Womens Hockey League](https://www.thepwhl.com/) | [Teams](https://github.com/IsabelleLefebvre97/PWHL-Data-Reference/blob/main/data/basic/teams.csv) | [HockeyTech](https://www.hockeytech.com/) |
62-
57+
Custom APIs can be configured for the following leagues using HockeyTech APIs
6358

59+
| League | `sport_path` | `league_path` |
60+
| --- | --- | --- |
61+
| [Professional Womens Hockey League](https://www.thepwhl.com/) | hockeytech | pwhl |
62+
| [Canadian Hockey League](https://chl.ca/) | hockeytech | chl |
63+
| [Ontario Hockey League](https://ontariohockeyleague.com/) | hockeytech | ohl |
64+
| [Western Hockey League](https://whl.ca/) | hockeytech | whl |
65+
| [Quebec Major Junior Hockey League](https://theqmjhl.ca/) | hockeytech | qmjhl |
66+
| [American Hockey League](https://theahl.com/) | hockeytech | ahl |
67+
| [East Coast Hockey League](https://echl.com/) | hockeytech | echl |
68+
| [United States Hockey League](https://ushl.com/) | hockeytech | ushl |
69+
| [Ontario Junior Hockey League](https://www.ojhl.ca/) | hockeytech | ojhl |
70+
| [British Columbia Hockey League](https://bchl.ca/) | hockeytech | bchl |
71+
| [Saskatchewan Junior Hockey League](https://sjhl.ca/) | hockeytech | sjhl |
72+
| [Alberta Junior Hockey League](https://www.ajhl.ca/) | hockeytech | ajhl |
73+
| [Manitoba Junior Hockey League](https://mjhlhockey.ca/) | hockeytech | mjhl |
74+
| [Maritime Junior Hockey League](https://www.themhl.ca/) | hockeytech | mhl |
6475
## Installation
6576

6677
### Manual Installation

custom_components/teamtracker/__init__.py

Lines changed: 70 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
CONF_SPORT_PATH,
3333
CONF_TEAM_ID,
3434
COORDINATOR,
35+
DATA_PROVIDER_ESPN,
3536
DEFAULT_KICKOFF_IN,
3637
DEFAULT_LAST_UPDATE,
3738
DEFAULT_LEAGUE,
3839
DEFAULT_LOGO,
3940
DEFAULT_TIMEOUT,
4041
DOMAIN,
41-
HOCKEYTECH_LEAGUES,
4242
ISSUE_URL,
4343
LEAGUE_MAP,
4444
PLATFORMS,
@@ -51,7 +51,11 @@
5151
VERSION,
5252
)
5353
from .event import async_process_event
54-
from .hockeytech import async_fetch_hockeytech_scoreboard
54+
from .hockeytech import (
55+
async_fetch_hockeytech_data,
56+
DATA_PROVIDER_HOCKEYTECH,
57+
RAPID_REFRESH_RATE_HOCKEYTECH,
58+
)
5559
from .utils import is_integer, async_call_espn_api, async_get_value, has_team
5660

5761
_LOGGER = logging.getLogger(__name__)
@@ -246,7 +250,12 @@ def __init__(self, hass, config, entry: ConfigEntry=None):
246250
self.league_id = config[CONF_LEAGUE_ID]
247251
self.league_path = config[CONF_LEAGUE_PATH]
248252
self.sport_path = config[CONF_SPORT_PATH]
253+
if self.sport_path.lower() == DATA_PROVIDER_HOCKEYTECH:
254+
self.data_provider = DATA_PROVIDER_HOCKEYTECH
255+
else:
256+
self.data_provider = DATA_PROVIDER_ESPN
249257
self.team_id = config[CONF_TEAM_ID]
258+
250259
self.conference_id = ""
251260
if CONF_CONFERENCE_ID in config.keys():
252261
if len(config[CONF_CONFERENCE_ID]) > 0:
@@ -333,7 +342,8 @@ async def async_get_team_schedule(self, lang):
333342
league_map = {}
334343
next_events = []
335344

336-
team_data = await async_call_espn_api(self.hass, sensor_name, team_id, team_url)
345+
response = await async_call_espn_api(self.hass, team_url, None, sensor_name, team_id)
346+
team_data = response["data"]
337347
if team_data:
338348
next_events = team_data.get("team", {}).get("nextEvent", [])
339349
for ne in next_events:
@@ -347,7 +357,8 @@ async def async_get_team_schedule(self, lang):
347357
league_map[str(eid)] = display
348358

349359
schedule_url = team_url + "/schedule"
350-
sched_data = await async_call_espn_api(self.hass, sensor_name, team_id, schedule_url)
360+
response = await async_call_espn_api(self.hass, schedule_url, None, sensor_name, team_id)
361+
sched_data = response["data"]
351362
if sched_data:
352363
for e in sched_data.get("events", []):
353364
eid = e.get("id")
@@ -394,7 +405,10 @@ async def _async_update_data(self):
394405
# update the interval based on flag
395406
if data["private_fast_refresh"]:
396407
if self.update_interval != RAPID_REFRESH_RATE:
397-
self.update_interval = RAPID_REFRESH_RATE
408+
if self.data_provider == DATA_PROVIDER_HOCKEYTECH:
409+
self.update_interval = RAPID_REFRESH_RATE_HOCKEYTECH
410+
else:
411+
self.update_interval = RAPID_REFRESH_RATE
398412
_LOGGER.debug(
399413
"%s: Switching to rapid refresh rate (%s)", self.name, self.update_interval
400414
)
@@ -441,15 +455,15 @@ async def async_update_sport_data(self, config, hass) -> dict:
441455

442456
if now < expiration:
443457
data = self.data_cache[key]
444-
values = await self.async_update_values(config, hass, data, lang)
458+
values = await self.async_update_values(hass, data, lang)
445459
if values["api_message"]:
446460
values["api_message"] = "Cached data: " + values["api_message"]
447461
else:
448462
values["api_message"] = "Cached data"
449463
return values
450464

451-
data = await self.async_call_sport_apis(config, hass, lang)
452-
values = await self.async_update_values(config, hass, data, lang)
465+
data = await self.async_call_sport_apis(hass, lang)
466+
values = await self.async_update_values(hass, data, lang)
453467

454468
if data is not None:
455469
self.data_cache[key] = data
@@ -478,24 +492,18 @@ async def async_update_sport_data(self, config, hass) -> dict:
478492
# This is the API dispatcher, calls to new non-ESPN API's should be added here based on league_path.
479493
# Response data should be formatted as an ESPN event.
480494
#
481-
async def async_call_sport_apis(self, config, hass, lang) -> dict:
495+
async def async_call_sport_apis(self, hass, lang) -> dict:
482496
"""Calls appropriate set of APIs based on sport and league."""
483497

484498
league_path = self.league_path
485-
486-
league_path_upper = league_path.upper()
487-
if league_path_upper in HOCKEYTECH_LEAGUES:
488-
session = async_get_clientsession(hass)
489-
data = await async_fetch_hockeytech_scoreboard(
490-
session=session,
491-
league_id=league_path_upper,
492-
sensor_name=self.name,
493-
)
494-
self.api_url = f"{HOCKEYTECH_LEAGUES[league_path_upper]['client_code']}.hockeytech.com/scorebar"
499+
if self.data_provider == DATA_PROVIDER_HOCKEYTECH:
500+
response = await async_fetch_hockeytech_data(hass, league_path.upper(), self.name, lang)
501+
data = response["data"]
502+
self.api_url = response["url"]
495503
elif (league_path == "all") and is_integer(self.team_id):
496-
data = await self.async_fetch_espn_all_leagues_data(config, hass, lang)
504+
data = await self.async_fetch_espn_all_leagues_data(hass, lang)
497505
else:
498-
data = await self.async_fetch_espn_data(config, hass, lang)
506+
data = await self.async_fetch_espn_data(hass, lang)
499507

500508
return data
501509

@@ -507,16 +515,17 @@ async def async_call_sport_apis(self, config, hass, lang) -> dict:
507515
# 2. Call w/o date range specfied (uses ESPN default behavior)
508516
# 3. Call w/o language parm (some sports not returned in some languages)
509517
#
510-
async def async_fetch_espn_data(self, config, hass, lang) -> dict:
518+
async def async_fetch_espn_data(self, hass, lang) -> dict:
511519
"""Gets data from ESPN APIs for specified league."""
512520

513521
sensor_name = self.name
514522
sport_path = self.sport_path
515523
league_path = self.league_path
516524
team_id = self.team_id.upper()
517525

518-
519-
url_parms = "?lang=" + lang[:2] + "&limit=" + str(API_LIMIT)
526+
url_parms = {}
527+
url_parms["lang"] = lang[:2]
528+
url_parms["limit"] = str(API_LIMIT)
520529

521530
if sport_path not in ("tennis"):
522531
d1 = (date.today() - timedelta(days=1)).strftime("%Y%m%d")
@@ -526,24 +535,19 @@ async def async_fetch_espn_data(self, config, hass, lang) -> dict:
526535
d2 = (date.today() + timedelta(days=1)).strftime("%Y%m%d")
527536
else:
528537
d2 = (date.today() + timedelta(days=90)).strftime("%Y%m%d")
529-
url_parms = url_parms + "&dates=" + d1 + "-" + d2
538+
url_parms["dates"] = f"{d1}-{d2}"
530539

531540
file_override = False
532541
if self.conference_id:
533-
url_parms = url_parms + "&groups=" + self.conference_id
542+
url_parms["groups"] = self.conference_id
534543
if self.conference_id == "9999":
535544
file_override = True
536545

537-
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
546+
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL
538547

539-
_LOGGER.debug(
540-
"%s: Calling API for '%s' from %s",
541-
sensor_name,
542-
team_id,
543-
url,
544-
)
545-
546-
data = await async_call_espn_api(hass, sensor_name, team_id, url, file_override)
548+
response = await async_call_espn_api(hass, url, url_parms, sensor_name, team_id, file_override)
549+
data = response["data"]
550+
url = response["url"]
547551

548552
num_events = 0
549553
if data is not None:
@@ -567,20 +571,13 @@ async def async_fetch_espn_data(self, config, hass, lang) -> dict:
567571

568572
# First fallback - without date constraint
569573
if num_events == 0:
570-
url_parms = "?lang=" + lang[:2]
571-
if self.conference_id:
572-
url_parms = url_parms + "&groups=" + self.conference_id
573-
574-
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
574+
url_parms.pop("dates", None)
575575

576-
_LOGGER.debug(
577-
"%s: Calling API without date constraint for '%s' from %s",
578-
sensor_name,
579-
team_id,
580-
url,
581-
)
576+
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL
582577

583-
data = await async_call_espn_api(hass, sensor_name, team_id, url)
578+
response = await async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
579+
data = response["data"]
580+
url = response["url"]
584581

585582
num_events = 0
586583
if data is not None:
@@ -604,19 +601,19 @@ async def async_fetch_espn_data(self, config, hass, lang) -> dict:
604601

605602
# Second fallback - without language
606603
if num_events == 0:
607-
url_parms = ""
608-
if self.conference_id:
609-
url_parms = url_parms + "?groups=" + self.conference_id
604+
url_parms.pop("lang", None)
610605

611-
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
606+
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL
612607
_LOGGER.debug(
613608
"%s: Calling API without language for '%s' from %s",
614609
sensor_name,
615610
team_id,
616611
url,
617612
)
618613

619-
data = await async_call_espn_api(hass, sensor_name, team_id, url)
614+
response = await async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
615+
data = response["data"]
616+
url = response["url"]
620617

621618
self.api_url = url
622619
return data
@@ -629,7 +626,7 @@ async def async_fetch_espn_data(self, config, hass, lang) -> dict:
629626
# 2. Call w/ date range up to upcoming game
630627
# 2. Call w/ date range around upcoming game
631628
#
632-
async def async_fetch_espn_all_leagues_data(self, config, hass, lang) -> dict:
629+
async def async_fetch_espn_all_leagues_data(self, hass, lang) -> dict:
633630
"""Gets data from ESPN APIs for all leagues in specified sport."""
634631

635632
sensor_name = self.name
@@ -657,18 +654,16 @@ async def async_fetch_espn_all_leagues_data(self, config, hass, lang) -> dict:
657654
next_game_date.isoformat() if next_game_date else "unknown",
658655
)
659656

660-
url_parms = "?lang=" + lang[:2] + "&limit=" + str(API_LIMIT)
661-
url_parms = url_parms + "&dates=" + d1 + "-" + d2
662-
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
657+
url_parms = {}
658+
url_parms["lang"] = lang[:2]
659+
url_parms["limit"] = str(API_LIMIT)
660+
url_parms["dates"] = f"{d1}-{d2}"
663661

664-
_LOGGER.debug(
665-
"%s: Calling API for '%s' from %s",
666-
sensor_name,
667-
team_id,
668-
url,
669-
)
662+
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL
670663

671-
data = await async_call_espn_api(hass, sensor_name, team_id, url)
664+
response = await async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
665+
data = response["data"]
666+
url = response["url"]
672667

673668
# If event for team not returned, narrow date range and try again
674669
if has_team(data, team_id) is False:
@@ -681,18 +676,13 @@ async def async_fetch_espn_all_leagues_data(self, config, hass, lang) -> dict:
681676
sensor_name, nd1, nd2,
682677
)
683678

684-
url_parms = "?lang=" + lang[:2] + "&limit=" + str(API_LIMIT)
685-
url_parms = url_parms + "&dates=" + nd1 + "-" + nd2
686-
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
679+
url_parms["dates"] = f"{nd1}-{nd2}"
680+
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL
687681

688-
_LOGGER.debug(
689-
"%s: Calling API for '%s' from %s",
690-
sensor_name,
691-
team_id,
692-
url,
693-
)
682+
response = await async_call_espn_api(hass, url, url_parms, sensor_name, team_id)
683+
data = response["data"]
684+
url = response["url"]
694685

695-
data = await async_call_espn_api(hass, sensor_name, team_id, url)
696686

697687
self.api_url = url
698688
return data
@@ -701,7 +691,7 @@ async def async_fetch_espn_all_leagues_data(self, config, hass, lang) -> dict:
701691
#
702692
# async_update_values()
703693
#
704-
async def async_update_values(self, config, hass, data, lang) -> dict:
694+
async def async_update_values(self, hass, data, lang) -> dict:
705695
"""Updates sensor values using data returned by API or in cache"""
706696

707697
sensor_name = self.name
@@ -711,7 +701,10 @@ async def async_update_values(self, config, hass, data, lang) -> dict:
711701
# Populate base values that do not need API data
712702
values = {}
713703
values = await async_clear_values()
714-
values["sport"] = self.sport_path
704+
if self.sport_path.lower() == "hockeytech":
705+
values["sport"] = "hockey"
706+
else:
707+
values["sport"] = self.sport_path
715708
values["sport_path"] = self.sport_path
716709
values["league"] = league_id
717710
values["league_path"] = self.league_path
@@ -755,7 +748,8 @@ async def async_update_values(self, config, hass, data, lang) -> dict:
755748
url = (
756749
f"{URL_HEAD}/{self.sport_path}/{self.league_path}/teams/{team_id}"
757750
)
758-
team_data = await async_call_espn_api(hass, sensor_name, team_id, url)
751+
response = await async_call_espn_api(hass, url, None, sensor_name, team_id)
752+
team_data = response["data"]
759753
if team_data:
760754
values["team_id"] = team_id
761755
values["team_abbr"] = await async_get_value(team_data, "team", "abbreviation", default=team_id)

custom_components/teamtracker/config_flow.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ async def _fetch_teams(hass: HomeAssistant, league_id: str, sport_path: str, lea
103103
league = paths[CONF_LEAGUE_PATH]
104104
url = (
105105
f"https://site.api.espn.com/apis/site/v2/sports"
106-
f"/{sport}/{league}/teams?limit=1000"
106+
f"/{sport}/{league}/teams"
107107
)
108-
data = await async_call_espn_api(hass, "ConfigFlow-teams", league, url)
109-
108+
url_parms = {"limit": 1000}
109+
response = await async_call_espn_api(hass, url, url_parms, "ConfigFlow-teams", league)
110+
data = response["data"]
110111
if data:
111112
raw = (
112113
data.get("sports", [{}])[0]
@@ -144,7 +145,8 @@ async def _fetch_team_conference_id(
144145
f"https://site.api.espn.com/apis/site/v2/sports"
145146
f"/{sport}/{league}/teams/{team_id}"
146147
)
147-
data = await async_call_espn_api(hass, "ConfigFlow-teamGroup", team_id, url)
148+
response = await async_call_espn_api(hass, url, None, "ConfigFlow-teamGroup", team_id)
149+
data = response["data"]
148150
if data:
149151
groups = data.get("team", {}).get("groups") or {}
150152
return str(groups.get("id", ""))

0 commit comments

Comments
 (0)