Skip to content

Commit 6032226

Browse files
authored
v0.14.10 Release Candidate (#248)
- Release lingering resources to meet tighter Python requirements - Update regression tests for Python 3.13 compatibility
1 parent e146837 commit 6032226

File tree

12 files changed

+259
-97
lines changed

12 files changed

+259
-97
lines changed

.github/workflows/pylint.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ["3.12"]
14+
python-version: ["3.13"]
1515
steps:
1616
- uses: actions/checkout@v4
1717
- name: Set up Python ${{ matrix.python-version }}
18-
uses: actions/setup-python@v5
18+
uses: actions/setup-python@v6
1919
with:
2020
python-version: ${{ matrix.python-version }}
2121
- name: Install dependencies

.github/workflows/pytest.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ['3.11', '3.12']
14+
python-version: ['3.11', '3.12', '3.13']
1515

1616
steps:
1717
- uses: actions/checkout@v4
1818
- name: Set up Python ${{ matrix.python-version }}
19-
uses: actions/setup-python@v5
19+
uses: actions/setup-python@v6
2020
with:
2121
python-version: ${{ matrix.python-version }}
2222
- name: Install dependencies
@@ -30,6 +30,6 @@ jobs:
3030
pip install pytest-cov
3131
pytest ./tests/ --cov=custom_components/teamtracker/ --cov-report=xml
3232
- name: Upload coverage to Codecov
33-
uses: codecov/codecov-action@v4
33+
uses: codecov/codecov-action@v5
3434
env:
3535
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

custom_components/teamtracker/__init__.py

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ async def async_call_api_service(call):
112112
VERSION,
113113
ISSUE_URL,
114114
)
115-
hass.data.setdefault(DOMAIN, {})
116115

116+
# Initialize DOMAIN in hass.data if it doesn't exist
117+
if DOMAIN not in hass.data:
118+
hass.data[DOMAIN] = {}
119+
117120
entry.async_on_unload(entry.add_update_listener(update_options_listener))
118121

119122
if entry.unique_id is not None:
@@ -153,6 +156,14 @@ async def async_call_api_service(call):
153156
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
154157
"""Handle removal of an entry."""
155158

159+
# Shut down the coordinator first to close aiohttp session
160+
if entry.entry_id in hass.data[DOMAIN]:
161+
coordinator = hass.data[DOMAIN][entry.entry_id].get(COORDINATOR)
162+
if coordinator:
163+
if hasattr(coordinator, "async_unload"):
164+
await coordinator.async_unload()
165+
166+
# Unload platforms
156167
unload_ok = all(
157168
await asyncio.gather(
158169
*[
@@ -164,6 +175,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
164175

165176
if unload_ok:
166177
hass.data[DOMAIN].pop(entry.entry_id)
178+
179+
# Only remove service if this is the last entry
180+
if not hass.data[DOMAIN]:
181+
hass.services.async_remove(DOMAIN, SERVICE_NAME_CALL_API)
182+
TeamTrackerDataUpdateCoordinator.data_cache.clear()
183+
TeamTrackerDataUpdateCoordinator.last_update.clear()
184+
TeamTrackerDataUpdateCoordinator.c_cache.clear()
167185

168186
return unload_ok
169187

@@ -227,12 +245,27 @@ def __init__(self, hass, config, entry: ConfigEntry=None):
227245
self.config = config
228246
self.hass = hass
229247
self.entry = entry #None if setup from YAML
248+
self._session = None # ADD: Track aiohttp session
230249

231250
super().__init__(hass, _LOGGER, name=self.name, update_interval=DEFAULT_REFRESH_RATE)
232251
_LOGGER.debug(
233252
"%s: Using default refresh rate (%s)", self.name, self.update_interval
234253
)
235254

255+
# ADD: New method to get or create session
256+
async def _get_session(self):
257+
"""Get or create aiohttp session."""
258+
if self._session is None or self._session.closed:
259+
self._session = aiohttp.ClientSession()
260+
return self._session
261+
262+
# ADD: New method to cleanup
263+
async def async_shutdown(self):
264+
"""Cleanup coordinator resources."""
265+
if self._session and not self._session.closed:
266+
await self._session.close()
267+
_LOGGER.debug("%s: Closed aiohttp session", self.name)
268+
236269

237270
#
238271
# Return the language to use for the API
@@ -401,19 +434,20 @@ async def async_call_api(self, config, hass, lang) -> dict:
401434
contents = await f.read()
402435
data = json.loads(contents)
403436
else:
404-
async with aiohttp.ClientSession() as session:
405-
try:
406-
async with session.get(url, headers=headers) as r:
407-
_LOGGER.debug(
408-
"%s: Calling API for '%s' from %s",
409-
sensor_name,
410-
team_id,
411-
url,
412-
)
413-
if r.status == 200:
414-
data = await r.json()
415-
except:
416-
data = None
437+
session = await self._get_session()
438+
try:
439+
async with session.get(url, headers=headers) as r:
440+
_LOGGER.debug(
441+
"%s: Calling API for '%s' from %s",
442+
sensor_name,
443+
team_id,
444+
url,
445+
)
446+
if r.status == 200:
447+
data = await r.json()
448+
except Exception as e: # pylint: disable=broad-exception-caught
449+
_LOGGER.debug("%s: API call failed: %s", sensor_name, e)
450+
data = None
417451

418452
num_events = 0
419453
if data is not None:
@@ -434,6 +468,8 @@ async def async_call_api(self, config, hass, lang) -> dict:
434468
num_events,
435469
url,
436470
)
471+
472+
# First fallback - without date constraint
437473
if num_events == 0:
438474
url_parms = "?lang=" + lang[:2]
439475
if self.conference_id:
@@ -443,19 +479,19 @@ async def async_call_api(self, config, hass, lang) -> dict:
443479

444480
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
445481

446-
async with aiohttp.ClientSession() as session:
447-
try:
448-
async with session.get(url, headers=headers) as r:
449-
_LOGGER.debug(
450-
"%s: Calling API without date constraint for '%s' from %s",
451-
sensor_name,
452-
team_id,
453-
url,
454-
)
455-
if r.status == 200:
456-
data = await r.json()
457-
except:
458-
data = None
482+
try:
483+
async with session.get(url, headers=headers) as r:
484+
_LOGGER.debug(
485+
"%s: Calling API without date constraint for '%s' from %s",
486+
sensor_name,
487+
team_id,
488+
url,
489+
)
490+
if r.status == 200:
491+
data = await r.json()
492+
except Exception as e: # pylint: disable=broad-exception-caught
493+
_LOGGER.debug("%s: API call failed: %s", sensor_name, e)
494+
data = None
459495

460496
num_events = 0
461497
if data is not None:
@@ -465,7 +501,6 @@ async def async_call_api(self, config, hass, lang) -> dict:
465501
team_id,
466502
url,
467503
)
468-
469504
try:
470505
num_events = len(data["events"])
471506
except:
@@ -478,6 +513,7 @@ async def async_call_api(self, config, hass, lang) -> dict:
478513
url,
479514
)
480515

516+
# Second fallback - without language
481517
if num_events == 0:
482518
url_parms = ""
483519
if self.conference_id:
@@ -487,23 +523,25 @@ async def async_call_api(self, config, hass, lang) -> dict:
487523

488524
url = URL_HEAD + sport_path + "/" + league_path + URL_TAIL + url_parms
489525

490-
async with aiohttp.ClientSession() as session:
491-
try:
492-
async with session.get(url, headers=headers) as r:
493-
_LOGGER.debug(
494-
"%s: Calling API without language for '%s' from %s",
495-
sensor_name,
496-
team_id,
497-
url,
498-
)
499-
if r.status == 200:
500-
data = await r.json()
501-
except:
502-
data = None
526+
try:
527+
async with session.get(url, headers=headers) as r:
528+
_LOGGER.debug(
529+
"%s: Calling API without language for '%s' from %s",
530+
sensor_name,
531+
team_id,
532+
url,
533+
)
534+
if r.status == 200:
535+
data = await r.json()
536+
except Exception as e: # pylint: disable=broad-exception-caught
537+
_LOGGER.debug("%s: API call failed: %s", sensor_name, e)
538+
data = None
539+
503540
self.api_url = url
504541

505542
return data, file_override
506543

544+
507545
async def async_update_values(self, config, hass, data, lang) -> dict:
508546
"""Return values based on the data passed into method"""
509547

0 commit comments

Comments
 (0)