-
-
Notifications
You must be signed in to change notification settings - Fork 37.1k
Add update integration #66552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add update integration #66552
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
e1cb86b
Repurpose the Updater integration
ludeeus 9b395da
Remove debug
ludeeus 014b9b8
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus 58c2e97
Pass updater_data to _get_update_details
ludeeus cda5149
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus bf1bce0
split update to own module, and leave updater as is
ludeeus e76b217
Add changelog_content
ludeeus cad343b
adjustments after split
ludeeus 7f5e525
Update script/scaffold/templates/update/integration/update.py
ludeeus 7324fe2
Remove scaffold
ludeeus 52554b9
update
ludeeus 4b4cb32
Add delay
ludeeus 3b4c4c7
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus 36a6d0f
fix type conflict
ludeeus ba2c188
Add missing coverage
ludeeus 05260ed
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus 0a7a43d
Load when needed
ludeeus 3c7b1b8
updates
ludeeus 3592149
rename WS function
ludeeus e846eb6
remove
ludeeus d95c552
Update homeassistant/components/update/__init__.py
ludeeus 0c914f5
Update homeassistant/components/update/__init__.py
ludeeus eefa61a
Move platform registration
ludeeus 6f0821a
Changes needed after suggestion
ludeeus 0f154ee
Only register /info during setup
ludeeus 5f4a41f
Better error
ludeeus 36b86f3
Use supported_features
ludeeus d6023cf
Move assert
ludeeus b184f3a
Use dataclasses.field
ludeeus 95741d2
Fix test
ludeeus c580b23
Adjust working
ludeeus e0a9e87
Remove assert
ludeeus 394ca7b
Load if not loaded in domain_is_valid
ludeeus d74e862
Remove _register_update_platform
ludeeus 69fa06e
Use supports_backup
ludeeus a4a8f1a
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus 29a4898
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus 0977a15
print the stack trace if it's an unexpected exception
ludeeus 51e87bf
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus a1a3475
Add UpdateFailed exception
ludeeus a4801eb
coverage
ludeeus 903d2ce
Apply suggestions from code review
ludeeus fda8db3
adjust
ludeeus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| """Support for Update.""" | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import dataclasses | ||
| import logging | ||
| from typing import Any, Protocol | ||
|
|
||
| import async_timeout | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components import websocket_api | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.exceptions import HomeAssistantError | ||
| from homeassistant.helpers import integration_platform, storage | ||
| from homeassistant.helpers.typing import ConfigType | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| DOMAIN = "update" | ||
| INFO_CALLBACK_TIMEOUT = 5 | ||
| STORAGE_VERSION = 1 | ||
|
|
||
|
|
||
| class IntegrationUpdateFailed(HomeAssistantError): | ||
| """Error to indicate an update has failed.""" | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
| """Set up the Update integration.""" | ||
| hass.data[DOMAIN] = UpdateManager(hass=hass) | ||
| websocket_api.async_register_command(hass, handle_info) | ||
| websocket_api.async_register_command(hass, handle_update) | ||
| websocket_api.async_register_command(hass, handle_skip) | ||
| return True | ||
|
|
||
|
|
||
| @websocket_api.websocket_command({vol.Required("type"): "update/info"}) | ||
| @websocket_api.async_response | ||
| async def handle_info( | ||
| hass: HomeAssistant, | ||
| connection: websocket_api.ActiveConnection, | ||
| msg: dict[str, Any], | ||
| ) -> None: | ||
| """Get pending updates from all platforms.""" | ||
| manager: UpdateManager = hass.data[DOMAIN] | ||
| updates = await manager.gather_updates() | ||
| connection.send_result(msg["id"], updates) | ||
|
|
||
|
|
||
| @websocket_api.websocket_command( | ||
| { | ||
| vol.Required("type"): "update/skip", | ||
| vol.Required("domain"): str, | ||
| vol.Required("identifier"): str, | ||
| vol.Required("version"): str, | ||
| } | ||
| ) | ||
| @websocket_api.async_response | ||
| async def handle_skip( | ||
| hass: HomeAssistant, | ||
| connection: websocket_api.ActiveConnection, | ||
| msg: dict[str, Any], | ||
| ) -> None: | ||
| """Skip an update.""" | ||
| manager: UpdateManager = hass.data[DOMAIN] | ||
|
|
||
| if not await manager.domain_is_valid(msg["domain"]): | ||
| connection.send_error( | ||
| msg["id"], websocket_api.ERR_NOT_FOUND, "Domain not supported" | ||
| ) | ||
| return | ||
|
|
||
| manager.skip_update(msg["domain"], msg["identifier"], msg["version"]) | ||
| connection.send_result(msg["id"]) | ||
|
|
||
|
|
||
| @websocket_api.websocket_command( | ||
| { | ||
| vol.Required("type"): "update/update", | ||
ludeeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| vol.Required("domain"): str, | ||
| vol.Required("identifier"): str, | ||
| vol.Required("version"): str, | ||
| vol.Optional("backup"): bool, | ||
| } | ||
| ) | ||
| @websocket_api.async_response | ||
| async def handle_update( | ||
| hass: HomeAssistant, | ||
| connection: websocket_api.ActiveConnection, | ||
| msg: dict[str, Any], | ||
| ) -> None: | ||
| """Handle an update.""" | ||
| manager: UpdateManager = hass.data[DOMAIN] | ||
|
|
||
| if not await manager.domain_is_valid(msg["domain"]): | ||
| connection.send_error( | ||
| msg["id"], | ||
| websocket_api.ERR_NOT_FOUND, | ||
| f"{msg['domain']} is not a supported domain", | ||
| ) | ||
| return | ||
|
|
||
| try: | ||
| await manager.perform_update( | ||
| domain=msg["domain"], | ||
| identifier=msg["identifier"], | ||
| version=msg["version"], | ||
| backup=msg.get("backup"), | ||
| ) | ||
| except IntegrationUpdateFailed as err: | ||
| connection.send_error( | ||
| msg["id"], | ||
| "update_failed", | ||
| str(err), | ||
| ) | ||
| except Exception: # pylint: disable=broad-except | ||
| _LOGGER.exception( | ||
ludeeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "Update of %s to version %s failed", | ||
| msg["identifier"], | ||
| msg["version"], | ||
| ) | ||
| connection.send_error( | ||
| msg["id"], | ||
| "update_failed", | ||
| "Unknown Error", | ||
| ) | ||
| else: | ||
| connection.send_result(msg["id"]) | ||
|
|
||
|
|
||
| class UpdatePlatformProtocol(Protocol): | ||
| """Define the format that update platforms can have.""" | ||
|
|
||
| async def async_list_updates(self, hass: HomeAssistant) -> list[UpdateDescription]: | ||
| """List all updates available in the integration.""" | ||
|
|
||
| async def async_perform_update( | ||
| self, | ||
| hass: HomeAssistant, | ||
| identifier: str, | ||
| version: str, | ||
| **kwargs: Any, | ||
| ) -> None: | ||
| """Perform an update.""" | ||
|
|
||
|
|
||
| @dataclasses.dataclass() | ||
| class UpdateDescription: | ||
| """Describe an update update.""" | ||
|
|
||
| identifier: str | ||
| name: str | ||
| current_version: str | ||
| available_version: str | ||
| changelog_content: str | None = None | ||
| changelog_url: str | None = None | ||
| icon_url: str | None = None | ||
| supports_backup: bool = False | ||
|
|
||
|
|
||
| class UpdateManager: | ||
| """Update manager for the update integration.""" | ||
|
|
||
| def __init__(self, hass: HomeAssistant) -> None: | ||
| """Initialize the update manager.""" | ||
| self._hass = hass | ||
| self._store = storage.Store( | ||
| hass=hass, | ||
| version=STORAGE_VERSION, | ||
| key=DOMAIN, | ||
| ) | ||
| self._skip: set[str] = set() | ||
| self._platforms: dict[str, UpdatePlatformProtocol] = {} | ||
| self._loaded = False | ||
|
|
||
| async def add_platform( | ||
| self, | ||
| hass: HomeAssistant, | ||
| integration_domain: str, | ||
| platform: UpdatePlatformProtocol, | ||
| ) -> None: | ||
| """Add a platform to the update manager.""" | ||
| self._platforms[integration_domain] = platform | ||
|
|
||
| async def _load(self) -> None: | ||
| """Load platforms and data from storage.""" | ||
| await integration_platform.async_process_integration_platforms( | ||
| self._hass, DOMAIN, self.add_platform | ||
| ) | ||
| from_storage = await self._store.async_load() | ||
| if isinstance(from_storage, dict): | ||
| self._skip = set(from_storage["skipped"]) | ||
|
|
||
| self._loaded = True | ||
|
|
||
| async def gather_updates(self) -> list[dict[str, Any]]: | ||
| """Gather updates.""" | ||
| if not self._loaded: | ||
| await self._load() | ||
|
|
||
| updates: dict[str, list[UpdateDescription] | None] = {} | ||
|
|
||
| for domain, update_descriptions in zip( | ||
| self._platforms, | ||
| await asyncio.gather( | ||
| *( | ||
| self._get_integration_info(integration_domain, registration) | ||
| for integration_domain, registration in self._platforms.items() | ||
| ) | ||
balloob marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ), | ||
| ): | ||
| updates[domain] = update_descriptions | ||
|
|
||
| return [ | ||
| { | ||
| "domain": integration_domain, | ||
| **dataclasses.asdict(description), | ||
| } | ||
| for integration_domain, update_descriptions in updates.items() | ||
| if update_descriptions is not None | ||
| for description in update_descriptions | ||
| if f"{integration_domain}_{description.identifier}_{description.available_version}" | ||
| not in self._skip | ||
| ] | ||
|
|
||
| async def domain_is_valid(self, domain: str) -> bool: | ||
| """Return if the domain is valid.""" | ||
| if not self._loaded: | ||
| await self._load() | ||
| return domain in self._platforms | ||
|
|
||
| @callback | ||
| def _data_to_save(self) -> dict[str, Any]: | ||
| """Schedule storing the data.""" | ||
| return {"skipped": list(self._skip)} | ||
|
|
||
| async def perform_update( | ||
| self, | ||
| domain: str, | ||
| identifier: str, | ||
| version: str, | ||
| **kwargs: Any, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When will the arbitrary keyword arguments be used?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for |
||
| ) -> None: | ||
| """Perform an update.""" | ||
| await self._platforms[domain].async_perform_update( | ||
| hass=self._hass, | ||
| identifier=identifier, | ||
| version=version, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| @callback | ||
| def skip_update(self, domain: str, identifier: str, version: str) -> None: | ||
| """Skip an update.""" | ||
| self._skip.add(f"{domain}_{identifier}_{version}") | ||
| self._store.async_delay_save(self._data_to_save, 60) | ||
|
|
||
| async def _get_integration_info( | ||
| self, | ||
| integration_domain: str, | ||
| platform: UpdatePlatformProtocol, | ||
| ) -> list[UpdateDescription] | None: | ||
| """Get integration update details.""" | ||
|
|
||
| try: | ||
| async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): | ||
| return await platform.async_list_updates(hass=self._hass) | ||
| except asyncio.TimeoutError: | ||
| _LOGGER.warning("Timeout while getting updates from %s", integration_domain) | ||
| except Exception: # pylint: disable=broad-except | ||
| _LOGGER.exception("Error fetching info from %s", integration_domain) | ||
| return None | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "domain": "update", | ||
| "name": "Update", | ||
| "documentation": "https://www.home-assistant.io/integrations/update", | ||
| "codeowners": [ | ||
| "@home-assistant/core" | ||
| ], | ||
| "quality_scale": "internal", | ||
| "iot_class": "calculated" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "title": "Update" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "title": "Update" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Tests for the Update integration.""" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if identifier or version is no longer valid ? We only validate domain. Would it help to drop
domain_is_validand just raise some exception if something is off?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we do not store updates anymore, that would mean that on each call info/update/skip we would need to gather all updates, then iterate over all updates for the domain to ensure the identifier and version are present.
That's expensive, and this I would say is a rare edge case, and if needed is something that the individual update platforms should handle, they get the identifier and version, and can raise or just return early if the update no longer exists.