Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e1cb86b
Repurpose the Updater integration
ludeeus Feb 15, 2022
9b395da
Remove debug
ludeeus Feb 15, 2022
014b9b8
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Feb 15, 2022
58c2e97
Pass updater_data to _get_update_details
ludeeus Feb 17, 2022
cda5149
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Feb 17, 2022
bf1bce0
split update to own module, and leave updater as is
ludeeus Feb 17, 2022
e76b217
Add changelog_content
ludeeus Feb 17, 2022
cad343b
adjustments after split
ludeeus Feb 17, 2022
7f5e525
Update script/scaffold/templates/update/integration/update.py
ludeeus Feb 17, 2022
7324fe2
Remove scaffold
ludeeus Feb 18, 2022
52554b9
update
ludeeus Feb 18, 2022
4b4cb32
Add delay
ludeeus Feb 18, 2022
3b4c4c7
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Feb 18, 2022
36a6d0f
fix type conflict
ludeeus Feb 18, 2022
ba2c188
Add missing coverage
ludeeus Feb 18, 2022
05260ed
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Feb 21, 2022
0a7a43d
Load when needed
ludeeus Feb 21, 2022
3c7b1b8
updates
ludeeus Feb 21, 2022
3592149
rename WS function
ludeeus Feb 21, 2022
e846eb6
remove
ludeeus Feb 21, 2022
d95c552
Update homeassistant/components/update/__init__.py
ludeeus Feb 21, 2022
0c914f5
Update homeassistant/components/update/__init__.py
ludeeus Feb 21, 2022
eefa61a
Move platform registration
ludeeus Feb 21, 2022
6f0821a
Changes needed after suggestion
ludeeus Feb 21, 2022
0f154ee
Only register /info during setup
ludeeus Feb 21, 2022
5f4a41f
Better error
ludeeus Feb 21, 2022
36b86f3
Use supported_features
ludeeus Feb 21, 2022
d6023cf
Move assert
ludeeus Feb 21, 2022
b184f3a
Use dataclasses.field
ludeeus Feb 21, 2022
95741d2
Fix test
ludeeus Feb 21, 2022
c580b23
Adjust working
ludeeus Feb 21, 2022
e0a9e87
Remove assert
ludeeus Feb 21, 2022
394ca7b
Load if not loaded in domain_is_valid
ludeeus Feb 21, 2022
d74e862
Remove _register_update_platform
ludeeus Feb 21, 2022
69fa06e
Use supports_backup
ludeeus Feb 21, 2022
a4a8f1a
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Feb 28, 2022
29a4898
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Mar 1, 2022
0977a15
print the stack trace if it's an unexpected exception
ludeeus Mar 1, 2022
51e87bf
Merge branch 'dev' of github.com:home-assistant/core into repurpose_u…
ludeeus Mar 2, 2022
a1a3475
Add UpdateFailed exception
ludeeus Mar 2, 2022
a4801eb
coverage
ludeeus Mar 2, 2022
903d2ce
Apply suggestions from code review
ludeeus Mar 3, 2022
fda8db3
adjust
ludeeus Mar 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .core_files.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ components: &components
- homeassistant/components/tag/*
- homeassistant/components/template/*
- homeassistant/components/timer/*
- homeassistant/components/update/*
- homeassistant/components/usb/*
- homeassistant/components/webhook/*
- homeassistant/components/websocket_api/*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,8 @@ tests/components/upb/* @gwww
homeassistant/components/upc_connect/* @pvizeli @fabaff
homeassistant/components/upcloud/* @scop
tests/components/upcloud/* @scop
homeassistant/components/update/* @home-assistant/core
tests/components/update/* @home-assistant/core
homeassistant/components/updater/* @home-assistant/core
tests/components/updater/* @home-assistant/core
homeassistant/components/upnp/* @StevenLooman @ehendrix23
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/default_config/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@
"tag",
"timer",
"usb",
"update",
"updater",
"webhook",
"zeroconf",
"zone"
],
"codeowners": [],
"quality_scale": "internal"
}
}
243 changes: 243 additions & 0 deletions homeassistant/components/update/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"""Support for Update."""
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
import dataclasses
import logging

import async_timeout
import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
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


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Update integration."""
store = storage.Store(
hass=hass,
version=STORAGE_VERSION,
key=DOMAIN,
)
hass.data[DOMAIN] = UpdateData(
store=store,
skip=set(await store.async_load() or []),
)

websocket_api.async_register_command(hass, handle_info)
websocket_api.async_register_command(hass, handle_update)
websocket_api.async_register_command(hass, handle_skip)

await integration_platform.async_process_integration_platforms(
Comment thread
ludeeus marked this conversation as resolved.
Outdated
hass, DOMAIN, _register_update_platform
)

return True


async def _register_update_platform(hass, integration_domain, platform) -> None:
"""Register a update platform."""
Comment thread
ludeeus marked this conversation as resolved.
Outdated
if hasattr(platform, "async_register"):
platform.async_register(UpdateRegistration(hass, integration_domain))
Comment thread
ludeeus marked this conversation as resolved.
Outdated


async def get_integration_info(
hass: HomeAssistant,
registration: UpdateRegistration,
) -> list[UpdateDescription] | None:
"""Get integration update details."""
assert registration.updates_callback

try:
async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT):
return await registration.updates_callback(hass)
except asyncio.TimeoutError:
_LOGGER.warning("Timeout while getting updates from %s", registration.domain)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error fetching info")
return None


def _get_update_details(
update_data: UpdateData,
domain: str,
identifier: str,
) -> UpdateDescription | None:
"""Get an update."""
return next(
(
update
for update in update_data.updates.get(domain, [])
if update.identifier == identifier
),
None,
)


def _filtered_updates(update_data: UpdateData) -> list[dict]:
"""Return a list of updates that are not skipped."""
return [
{
"domain": domain,
"identifier": description.identifier,
"name": description.name,
"current_version": description.current_version,
"available_version": description.available_version,
"changelog_url": description.changelog_url,
"icon_url": description.icon_url,
}
for domain, domain_data in update_data.updates.items()
if domain_data is not None
for description in domain_data
if f"{domain}_{description.identifier}_{description.available_version}"
not in update_data.skip
]


@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,
):
"""Get pending updates from all platforms."""
update_data: UpdateData = hass.data[DOMAIN]

for domain, domain_data in zip(
update_data.registrations,
await asyncio.gather(
*(
get_integration_info(hass, registration)
for registration in update_data.registrations.values()
)
),
):
update_data.updates[domain] = domain_data

connection.send_result(msg["id"], _filtered_updates(update_data))


@websocket_api.websocket_command(
{
vol.Required("type"): "update/skip",
vol.Required("domain"): str,
vol.Required("identifier"): str,
}
)
@websocket_api.async_response
async def handle_skip(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict,
):
"""Skip an update."""
update_data: UpdateData = hass.data[DOMAIN]
update_details = _get_update_details(update_data, msg["domain"], msg["identifier"])
if update_details is not None:
update_data.skip.add(
f"{msg['domain']}_{update_details.identifier}_{update_details.available_version}"
)
update_data.updates[msg["domain"]].remove(update_details)
await update_data.store.async_save(list(update_data.skip))
Comment thread
ludeeus marked this conversation as resolved.
Outdated

connection.send_result(
msg["id"],
_filtered_updates(update_data),
)


@websocket_api.websocket_command(
{
vol.Required("type"): "update/update",
Comment thread
ludeeus marked this conversation as resolved.
vol.Required("domain"): str,
vol.Required("identifier"): str,
}
)
@websocket_api.async_response
async def handle_update(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict,
):
"""Handle an update."""
update_data: UpdateData = hass.data[DOMAIN]
update_details = _get_update_details(update_data, msg["domain"], msg["identifier"])

if update_details is None:
connection.send_error(
msg["id"],
"not_found",
f"No updates found for {msg['domain']} and {msg['identifier']}",
)
return

if not await update_details.update_callback(hass, update_details):
connection.send_error(msg["id"], "update_failed", "Update failed")
Comment thread
ludeeus marked this conversation as resolved.
Outdated
return

update_data.updates[msg["domain"]].remove(update_details)

connection.send_result(
msg["id"],
_filtered_updates(update_data),
)


@dataclasses.dataclass()
class UpdateData:
"""Data for the update integration."""

store: storage.Store
skip: set[str]
registrations: dict[str, UpdateRegistration] = dataclasses.field(
default_factory=dict
)
updates: dict[str, list[UpdateDescription]] = dataclasses.field(
default_factory=dict
)


@dataclasses.dataclass()
class UpdateDescription:
"""Describe an update update."""

identifier: str
name: str
current_version: str
available_version: str
update_callback: Callable[[HomeAssistant, UpdateDescription], Awaitable[bool]]
changelog_content: str | None = None
changelog_url: str | None = None
icon_url: str | None = None


@dataclasses.dataclass()
class UpdateRegistration:
"""Helper class to track platform registration."""

hass: HomeAssistant
domain: str
updates_callback: Callable[
[HomeAssistant], Awaitable[list[UpdateDescription]]
] | None = None

@callback
def async_register(
self,
updates_callback: Callable[[HomeAssistant], Awaitable[list[UpdateDescription]]],
):
"""Register the updates info callback."""
update_data: UpdateData = self.hass.data[DOMAIN]
self.updates_callback = updates_callback
update_data.registrations[self.domain] = self
10 changes: 10 additions & 0 deletions homeassistant/components/update/manifest.json
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"
}
3 changes: 3 additions & 0 deletions homeassistant/components/update/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Update"
}
3 changes: 3 additions & 0 deletions homeassistant/components/update/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Update"
}
5 changes: 5 additions & 0 deletions script/scaffold/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"title": "Integration",
"docs": "https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html",
},
"update": {
"title": "Update",
"docs": "https://developers.home-assistant.io/docs/en/update.html",
"extra": "This will allow you to present updates the user can perform.",
},
"reproduce_state": {
"title": "Reproduce State",
"docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html",
Expand Down
27 changes: 27 additions & 0 deletions script/scaffold/templates/update/integration/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Update support for the NEW_NAME integration."""

from homeassistant.components.update import UpdateDescription, UpdateRegistration
from homeassistant.core import HomeAssistant, callback


@callback
def async_register(registration: UpdateRegistration) -> None:
"""Register the update handler."""
registration.async_register(get_pending_updates)


async def get_pending_updates(hass: HomeAssistant) -> list[UpdateDescription]:
"""Get pending updates."""
# TODO: Add your logic to gather updates here.
# And return a list of UpdateDescription objects.
return []


async def handle_update(
hass: HomeAssistant,
update_details: UpdateDescription,
) -> bool:
"""Handle an update."""
# TODO: Add your logic to perform updates here.
# And return a bool to indicate if the update where successful or not.
return True
10 changes: 10 additions & 0 deletions script/scaffold/templates/update/tests/test_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Test the NEW_NAME update platform."""
from homeassistant.components.NEW_DOMAIN.const import DOMAIN
from homeassistant.core import HomeAssistant

from tests.common import get_integration_updates


async def test_pending_updates(hass: HomeAssistant) -> None:
"""Test getting NEW_NAME updates."""
assert await get_integration_updates(hass, DOMAIN) == []
15 changes: 15 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
_async_get_device_automation_capabilities as async_get_device_automation_capabilities,
)
from homeassistant.components.mqtt.models import ReceiveMessage
from homeassistant.components.update import (
DOMAIN as UPDATE_DOMAIN,
UpdateData,
UpdateDescription,
)
from homeassistant.config import async_process_component_config
from homeassistant.const import (
DEVICE_DEFAULT_NAME,
Expand Down Expand Up @@ -1129,6 +1134,16 @@ async def get_system_health_info(hass, domain):
return await hass.data["system_health"][domain].info_callback(hass)


async def get_integration_updates(
hass: HomeAssistant,
domain: str,
) -> list[UpdateDescription]:
"""Get updates for an integration."""
await async_setup_component(hass, UPDATE_DOMAIN, {})
update_data: UpdateData = hass.data[UPDATE_DOMAIN]
Comment thread
ludeeus marked this conversation as resolved.
Outdated
return await update_data.registrations[domain].updates_callback(hass)


def mock_integration(hass, module, built_in=True):
"""Mock an integration."""
integration = loader.Integration(
Expand Down
1 change: 1 addition & 0 deletions tests/components/update/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Update integration."""
Loading