From e65670fef459016b465ab1c8d2ae10732d14803d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Feb 2022 15:56:36 +0100 Subject: [PATCH] Repository event subscription (#67284) --- homeassistant/components/github/__init__.py | 6 +++- homeassistant/components/github/const.py | 13 +++++++- .../components/github/coordinator.py | 30 +++++++++++++++++-- tests/components/github/common.py | 5 ++++ tests/components/github/test_sensor.py | 14 +++++++-- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 4ecff6e9648..404aeae11b5 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -38,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() + await coordinator.subscribe() hass.data[DOMAIN][repository] = coordinator @@ -45,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - return True @@ -77,6 +77,10 @@ def async_cleanup_device_registry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + for coordinator in repositories.values(): + coordinator.unsubscribe() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index efe9d7baa5e..a186f4684b3 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -11,7 +11,18 @@ DOMAIN = "github" CLIENT_ID = "1440cafcc86e3ea5d6a2" DEFAULT_REPOSITORIES = ["home-assistant/core", "esphome/esphome"] -DEFAULT_UPDATE_INTERVAL = timedelta(seconds=300) +FALLBACK_UPDATE_INTERVAL = timedelta(hours=1, minutes=30) CONF_ACCESS_TOKEN = "access_token" CONF_REPOSITORIES = "repositories" + + +REFRESH_EVENT_TYPES = ( + "CreateEvent", + "ForkEvent", + "IssuesEvent", + "PullRequestEvent", + "PushEvent", + "ReleaseEvent", + "WatchEvent", +) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 9723d3600f6..679c3d89aeb 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -6,15 +6,17 @@ from typing import Any from aiogithubapi import ( GitHubAPI, GitHubConnectionException, + GitHubEventModel, GitHubException, GitHubRatelimitException, GitHubResponseModel, ) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_UPDATE_INTERVAL, LOGGER +from .const import FALLBACK_UPDATE_INTERVAL, LOGGER, REFRESH_EVENT_TYPES GRAPHQL_REPOSITORY_QUERY = """ query ($owner: String!, $repository: String!) { @@ -109,13 +111,14 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.repository = repository self._client = client self._last_response: GitHubResponseModel[dict[str, Any]] | None = None + self._subscription_id: str | None = None self.data = {} super().__init__( hass, LOGGER, name=repository, - update_interval=DEFAULT_UPDATE_INTERVAL, + update_interval=FALLBACK_UPDATE_INTERVAL, ) async def _async_update_data(self) -> GitHubResponseModel[dict[str, Any]]: @@ -136,3 +139,26 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): else: self._last_response = response return response.data["data"]["repository"] + + async def _handle_event(self, event: GitHubEventModel) -> None: + """Handle an event.""" + if event.type in REFRESH_EVENT_TYPES: + await self.async_request_refresh() + + @staticmethod + async def _handle_error(error: GitHubException) -> None: + """Handle an error.""" + LOGGER.error("An error occurred while processing new events - %s", error) + + async def subscribe(self) -> None: + """Subscribe to repository events.""" + self._subscription_id = await self._client.repos.events.subscribe( + self.repository, + event_callback=self._handle_event, + error_callback=self._handle_error, + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.unsubscribe) + + def unsubscribe(self, *args) -> None: + """Unsubscribe to repository events.""" + self._client.repos.events.unsubscribe(subscription_id=self._subscription_id) diff --git a/tests/components/github/common.py b/tests/components/github/common.py index a75a8cfaa78..4bd26183299 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -31,6 +31,11 @@ async def setup_github_integration( }, headers=headers, ) + aioclient_mock.get( + f"https://api.github.com/repos/{repository}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=json.loads(load_fixture("graphql.json", DOMAIN)), diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index cba787cbc28..892fb8956e6 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -1,10 +1,12 @@ """Test GitHub sensor.""" import json -from homeassistant.components.github.const import DEFAULT_UPDATE_INTERVAL, DOMAIN +from homeassistant.components.github.const import DOMAIN, FALLBACK_UPDATE_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.util import dt +from .common import TEST_REPOSITORY + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -22,15 +24,21 @@ async def test_sensor_updates_with_empty_release_array( response_json = json.loads(load_fixture("graphql.json", DOMAIN)) response_json["data"]["repository"]["release"] = None + headers = json.loads(load_fixture("base_headers.json", DOMAIN)) aioclient_mock.clear_requests() + aioclient_mock.get( + f"https://api.github.com/repos/{TEST_REPOSITORY}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=response_json, - headers=json.loads(load_fixture("base_headers.json", DOMAIN)), + headers=headers, ) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt.utcnow() + FALLBACK_UPDATE_INTERVAL) await hass.async_block_till_done() new_state = hass.states.get(TEST_SENSOR_ENTITY)