Skip to content

Commit

Permalink
Add coordinator for update entities (#3572)
Browse files Browse the repository at this point in the history
* Add coordinator for update entities

* Handle None-timestamps

* Avoid changes in QueueManager

* Update snapshots

* Don't queue coordinator update

---------

Co-authored-by: Joakim Sørensen <[email protected]>
  • Loading branch information
emontnemery and ludeeus authored May 7, 2024
1 parent 9a0e0ba commit c0a1d6e
Show file tree
Hide file tree
Showing 17 changed files with 117 additions and 28 deletions.
32 changes: 31 additions & 1 deletion custom_components/hacs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)

from .const import DOMAIN, TV, URL_BASE
from .coordinator import HacsUpdateCoordinator
from .data_client import HacsDataClient
from .enums import (
ConfigurationType,
Expand Down Expand Up @@ -374,6 +375,7 @@ def __init__(self) -> None:
"""Initialize."""
self.common = HacsCommon()
self.configuration = HacsConfiguration()
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
self.core = HacsCore()
self.log = LOGGER
self.recurring_tasks: list[Callable[[], None]] = []
Expand Down Expand Up @@ -424,12 +426,14 @@ def enable_hacs_category(self, category: HacsCategory) -> None:
if category not in self.common.categories:
self.log.info("Enable category: %s", category)
self.common.categories.add(category)
self.coordinators[category] = HacsUpdateCoordinator()

def disable_hacs_category(self, category: HacsCategory) -> None:
"""Disable HACS category."""
if category in self.common.categories:
self.log.info("Disabling category: %s", category)
self.common.categories.pop(category)
self.coordinators.pop(category)

async def async_save_file(self, file_path: str, content: Any) -> bool:
"""Save a file."""
Expand Down Expand Up @@ -908,6 +912,7 @@ async def async_get_category_repositories_experimental(self, category: str) -> N
self.repositories.unregister(repository)

self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
self.coordinators[category].async_update_listeners()

async def async_get_category_repositories(self, category: HacsCategory) -> None:
"""Get repositories from category."""
Expand Down Expand Up @@ -1073,12 +1078,37 @@ async def async_update_downloaded_custom_repositories(self, _=None) -> None:
return
self.log.info("Starting recurring background task for downloaded custom repositories")

repositories_to_update = 0
repositories_updated = asyncio.Event()

async def update_repository(repository: HacsRepository) -> None:
"""Update a repository"""
nonlocal repositories_to_update
await repository.update_repository(ignore_issues=True)
repositories_to_update -= 1
if not repositories_to_update:
repositories_updated.set()

for repository in self.repositories.list_downloaded:
if (
repository.data.category in self.common.categories
and not self.repositories.is_default(repository.data.id)
):
self.queue.add(repository.update_repository(ignore_issues=True))
repositories_to_update += 1
self.queue.add(update_repository(repository))

async def update_coordinators() -> None:
"""Update all coordinators."""
await repositories_updated.wait()
for coordinator in self.coordinators.values():
coordinator.async_update_listeners()

if config_entry := self.configuration.config_entry:
config_entry.async_create_background_task(
self.hass, update_coordinators(), "update_coordinators"
)
else:
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")

self.log.debug("Recurring background task for downloaded custom repositories done")

Expand Down
37 changes: 37 additions & 0 deletions custom_components/hacs/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Coordinator to trigger entity updates."""
from __future__ import annotations

from collections.abc import Callable
from typing import Any, Callable

from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol


class HacsUpdateCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Dispatch updates to update entities."""

def __init__(self) -> None:
"""Initialize."""
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}

@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""

@callback
def remove_listener() -> None:
"""Remove update listener."""
self._listeners.pop(remove_listener)

self._listeners[remove_listener] = (update_callback, context)

return remove_listener

@callback
def async_update_listeners(self) -> None:
"""Update all registered listeners."""
for update_callback, _ in list(self._listeners.values()):
update_callback()
36 changes: 28 additions & 8 deletions custom_components/hacs/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity

from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
from .coordinator import HacsUpdateCoordinator
from .enums import HacsDispatchEvent, HacsGitHubRepo

if TYPE_CHECKING:
Expand Down Expand Up @@ -39,6 +41,10 @@ def __init__(self, hacs: HacsBase) -> None:
"""Initialize."""
self.hacs = hacs


class HacsDispatcherEntity(HacsBaseEntity):
"""Base HACS entity listening to dispatcher signals."""

async def async_added_to_hass(self) -> None:
"""Register for status events."""
self.async_on_remove(
Expand All @@ -64,7 +70,7 @@ def _update_and_write_state(self, _: Any) -> None:
self.async_write_ha_state()


class HacsSystemEntity(HacsBaseEntity):
class HacsSystemEntity(HacsDispatcherEntity):
"""Base system entity."""

_attr_icon = "hacs:hacs"
Expand All @@ -76,7 +82,7 @@ def device_info(self) -> dict[str, any]:
return system_info(self.hacs)


class HacsRepositoryEntity(HacsBaseEntity):
class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
"""Base repository entity."""

def __init__(
Expand All @@ -85,9 +91,11 @@ def __init__(
repository: HacsRepository,
) -> None:
"""Initialize."""
super().__init__(hacs=hacs)
BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
HacsBaseEntity.__init__(self, hacs=hacs)
self.repository = repository
self._attr_unique_id = str(repository.data.id)
self._repo_last_fetched = repository.data.last_fetched

@property
def available(self) -> bool:
Expand All @@ -112,8 +120,20 @@ def device_info(self) -> dict[str, any]:
}

@callback
def _update_and_write_state(self, data: dict) -> None:
"""Update the entity and write state."""
if data.get("repository_id") == self.repository.data.id:
self._update()
self.async_write_ha_state()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._repo_last_fetched is not None
and self.repository.data.last_fetched is not None
and self._repo_last_fetched >= self.repository.data.last_fetched
):
return

self._repo_last_fetched = self.repository.data.last_fetched
self.async_write_ha_state()

async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/appdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def update_repository(self, ignore_issues=False, force=False):
# Set local path
self.content.path.local = self.localpath

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async def update_repository(self, ignore_issues=False, force=False):
# Set local path
self.content.path.local = self.localpath

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/netdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async def update_repository(self, ignore_issues=False, force=False):
# Set local path
self.content.path.local = self.localpath

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def update_repository(self, ignore_issues=False, force=False):
if self.content.path.remote == "release":
self.content.single = True

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/python_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async def update_repository(self, ignore_issues=False, force=False):
# Update name
self.update_filenames()

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def update_repository(self, ignore_issues=False, force=False):
self.data.file_name = self.repository_manifest.filename
self.content.path.local = self.localpath

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hacs/repositories/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def update_repository(self, ignore_issues=False, force=False):
self.update_filenames()
self.content.path.local = self.localpath

# Signal entities to refresh
# Signal frontend to refresh
if self.data.installed:
self.hacs.async_dispatch(
HacsDispatchEvent.REPOSITORY,
Expand Down
2 changes: 2 additions & 0 deletions custom_components/hacs/websocket/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ async def hacs_repository_refresh(

await repository.update_repository(ignore_issues=True, force=True)
await hacs.data.async_write()
# Update state of update entity
hacs.coordinators[repository.data.category].async_update_listeners()

connection.send_message(websocket_api.result_message(msg["id"], {}))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"friendly_name": "Appdaemon Basic update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/appdaemon-basic",
"skipped_version": null,
Expand All @@ -37,6 +37,6 @@
"user_id": null
},
"entity_id": "update.appdaemon_basic_update",
"state": "off"
"state": "on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"friendly_name": "Basic integration update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/integration-basic",
"skipped_version": null,
Expand All @@ -39,6 +39,6 @@
"user_id": null
},
"entity_id": "update.basic_integration_update",
"state": "off"
"state": "on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"friendly_name": "Plugin Basic update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/plugin-basic",
"skipped_version": null,
Expand All @@ -37,6 +37,6 @@
"user_id": null
},
"entity_id": "update.plugin_basic_update",
"state": "off"
"state": "on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"friendly_name": "Python Script Basic update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/python_script-basic",
"skipped_version": null,
Expand All @@ -37,6 +37,6 @@
"user_id": null
},
"entity_id": "update.python_script_basic_update",
"state": "off"
"state": "on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"friendly_name": "Template Basic update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/template-basic",
"skipped_version": null,
Expand All @@ -37,6 +37,6 @@
"user_id": null
},
"entity_id": "update.template_basic_update",
"state": "off"
"state": "on"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"friendly_name": "Theme Basic update",
"in_progress": false,
"installed_version": "1.0.0",
"latest_version": "1.0.0",
"latest_version": "2.0.0",
"release_summary": null,
"release_url": "https://github.com/hacs-test-org/theme-basic",
"skipped_version": null,
Expand All @@ -37,6 +37,6 @@
"user_id": null
},
"entity_id": "update.theme_basic_update",
"state": "off"
"state": "on"
}
}

0 comments on commit c0a1d6e

Please sign in to comment.