Add update platform to the Supervisor integration (#68475)

This commit is contained in:
Joakim Sørensen 2022-03-22 12:21:12 +01:00 committed by GitHub
parent 1b955970f8
commit d17f8e9ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 901 additions and 19 deletions

View file

@ -51,6 +51,7 @@ from .auth import async_setup_auth_view
from .const import (
ATTR_ADDON,
ATTR_ADDONS,
ATTR_CHANGELOG,
ATTR_DISCOVERY,
ATTR_FOLDERS,
ATTR_HOMEASSISTANT,
@ -63,6 +64,9 @@ from .const import (
ATTR_URL,
ATTR_VERSION,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_OS,
DATA_KEY_SUPERVISOR,
DOMAIN,
SupervisorEntityModel,
)
@ -77,7 +81,7 @@ _LOGGER = logging.getLogger(__name__)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE]
CONF_FRONTEND_REPO = "development_repo"
@ -93,6 +97,7 @@ DATA_STORE = "hassio_store"
DATA_INFO = "hassio_info"
DATA_OS_INFO = "hassio_os_info"
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
DATA_ADDONS_STATS = "hassio_addons_stats"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
@ -239,14 +244,22 @@ async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict:
@bind_hass
@api_data
async def async_update_addon(hass: HomeAssistant, slug: str) -> dict:
async def async_update_addon(
hass: HomeAssistant,
slug: str,
backup: bool = False,
) -> dict:
"""Update add-on.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
command = f"/addons/{slug}/update"
return await hassio.send_command(command, timeout=None)
return await hassio.send_command(
command,
payload={"backup": backup},
timeout=None,
)
@bind_hass
@ -323,6 +336,52 @@ async def async_create_backup(
return await hassio.send_command(command, payload=payload, timeout=None)
@bind_hass
@api_data
async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict:
"""Update Home Assistant Operating System.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
command = "/os/update"
return await hassio.send_command(
command,
payload={"version": version},
timeout=None,
)
@bind_hass
@api_data
async def async_update_supervisor(hass: HomeAssistant) -> dict:
"""Update Home Assistant Supervisor.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
command = "/supervisor/update"
return await hassio.send_command(command, timeout=None)
@bind_hass
@api_data
async def async_update_core(
hass: HomeAssistant, version: str | None = None, backup: bool = False
) -> dict:
"""Update Home Assistant Core.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
command = "/core/update"
return await hassio.send_command(
command,
payload={"version": version, "backup": backup},
timeout=None,
)
@callback
@bind_hass
def get_info(hass):
@ -373,6 +432,16 @@ def get_addons_stats(hass):
return hass.data.get(DATA_ADDONS_STATS)
@callback
@bind_hass
def get_addons_changelogs(hass):
"""Return Addons changelogs.
Async friendly.
"""
return hass.data.get(DATA_ADDONS_CHANGELOGS)
@callback
@bind_hass
def get_os_info(hass):
@ -533,6 +602,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
stats = await hassio.get_addon_stats(slug)
return (slug, stats)
async def update_addon_changelog(slug):
"""Return the changelog for an add-on."""
changelog = await hassio.get_addon_changelog(slug)
return (slug, changelog)
async def update_info_data(now):
"""Update last available supervisor information."""
@ -562,6 +636,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
*[update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
)
hass.data[DATA_ADDONS_STATS] = dict(stats_data)
hass.data[DATA_ADDONS_CHANGELOGS] = dict(
await asyncio.gather(
*[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
)
)
if ADDONS_COORDINATOR in hass.data:
await hass.data[ADDONS_COORDINATOR].async_refresh()
@ -699,6 +778,42 @@ def async_register_os_in_dev_reg(
dev_reg.async_get_or_create(config_entry_id=entry_id, **params)
@callback
def async_register_core_in_dev_reg(
entry_id: str,
dev_reg: DeviceRegistry,
core_dict: dict[str, Any],
) -> None:
"""Register OS in the device registry."""
params = DeviceInfo(
identifiers={(DOMAIN, "core")},
manufacturer="Home Assistant",
model=SupervisorEntityModel.CORE,
sw_version=core_dict[ATTR_VERSION],
name="Home Assistant Core",
entry_type=DeviceEntryType.SERVICE,
)
dev_reg.async_get_or_create(config_entry_id=entry_id, **params)
@callback
def async_register_supervisor_in_dev_reg(
entry_id: str,
dev_reg: DeviceRegistry,
supervisor_dict: dict[str, Any],
) -> None:
"""Register OS in the device registry."""
params = DeviceInfo(
identifiers={(DOMAIN, "supervisor")},
manufacturer="Home Assistant",
model=SupervisorEntityModel.SUPERVIOSR,
sw_version=supervisor_dict[ATTR_VERSION],
name="Home Assistant Supervisor",
entry_type=DeviceEntryType.SERVICE,
)
dev_reg.async_get_or_create(config_entry_id=entry_id, **params)
@callback
def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None:
"""Remove addons from the device registry."""
@ -720,6 +835,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
name=DOMAIN,
update_method=self._async_update_data,
)
self.hassio: HassIO = hass.data[DOMAIN]
self.data = {}
self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg
@ -730,6 +846,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
new_data = {}
supervisor_info = get_supervisor_info(self.hass)
addons_stats = get_addons_stats(self.hass)
addons_changelogs = get_addons_changelogs(self.hass)
store_data = get_store(self.hass)
repositories = {
@ -741,6 +858,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
addon[ATTR_SLUG]: {
**addon,
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
ATTR_REPOSITORY: repositories.get(
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
),
@ -748,16 +866,25 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
for addon in supervisor_info.get("addons", [])
}
if self.is_hass_os:
new_data["os"] = get_os_info(self.hass)
new_data[DATA_KEY_OS] = get_os_info(self.hass)
new_data[DATA_KEY_CORE] = get_core_info(self.hass)
new_data[DATA_KEY_SUPERVISOR] = supervisor_info
# If this is the initial refresh, register all addons and return the dict
if not self.data:
async_register_addons_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
)
async_register_core_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE]
)
async_register_supervisor_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR]
)
if self.is_hass_os:
async_register_os_in_dev_reg(
self.entry_id, self.dev_reg, new_data["os"]
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
)
# Remove add-ons that are no longer installed from device registry
@ -782,3 +909,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
return {}
return new_data
async def force_info_update_supervisor(self) -> None:
"""Force update of the supervisor info."""
self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
await self.async_refresh()

View file

@ -32,6 +32,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription):
COMMON_ENTITY_DESCRIPTIONS = (
HassioBinarySensorEntityDescription(
# Deprecated, scheduled to be removed in 2022.6
device_class=BinarySensorDeviceClass.UPDATE,
entity_registry_enabled_default=False,
key=ATTR_UPDATE_AVAILABLE,

View file

@ -43,6 +43,7 @@ ATTR_VERSION = "version"
ATTR_VERSION_LATEST = "version_latest"
ATTR_UPDATE_AVAILABLE = "update_available"
ATTR_CPU_PERCENT = "cpu_percent"
ATTR_CHANGELOG = "changelog"
ATTR_MEMORY_PERCENT = "memory_percent"
ATTR_SLUG = "slug"
ATTR_STATE = "state"
@ -53,6 +54,8 @@ ATTR_REPOSITORY = "repository"
DATA_KEY_ADDONS = "addons"
DATA_KEY_OS = "os"
DATA_KEY_SUPERVISOR = "supervisor"
DATA_KEY_CORE = "core"
class SupervisorEntityModel(str, Enum):
@ -60,3 +63,5 @@ class SupervisorEntityModel(str, Enum):
ADDON = "Home Assistant Add-on"
OS = "Home Assistant Operating System"
CORE = "Home Assistant Core"
SUPERVIOSR = "Home Assistant Supervisor"

View file

@ -8,7 +8,13 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, HassioDataUpdateCoordinator
from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS
from .const import (
ATTR_SLUG,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_OS,
DATA_KEY_SUPERVISOR,
)
class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
@ -33,8 +39,9 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Return True if entity is available."""
return (
super().available
and DATA_KEY_ADDONS in self.coordinator.data
and self.entity_description.key
in self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug]
in self.coordinator.data[DATA_KEY_ADDONS].get(self._addon_slug, {})
)
@ -58,5 +65,57 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Return True if entity is available."""
return (
super().available
and DATA_KEY_OS in self.coordinator.data
and self.entity_description.key in self.coordinator.data[DATA_KEY_OS]
)
class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Base Entity for Supervisor."""
def __init__(
self,
coordinator: HassioDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize base entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_name = f"Home Assistant Supervisor: {entity_description.name}"
self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}"
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")})
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and DATA_KEY_OS in self.coordinator.data
and self.entity_description.key
in self.coordinator.data[DATA_KEY_SUPERVISOR]
)
class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Base Entity for Core."""
def __init__(
self,
coordinator: HassioDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize base entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_name = f"Home Assistant Core: {entity_description.name}"
self._attr_unique_id = f"home_assistant_core_{entity_description.key}"
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")})
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and DATA_KEY_CORE in self.coordinator.data
and self.entity_description.key in self.coordinator.data[DATA_KEY_CORE]
)

View file

@ -127,6 +127,15 @@ class HassIO:
"""
return self.send_command(f"/addons/{addon}/stats", method="get")
def get_addon_changelog(self, addon):
"""Return changelog for an Add-on.
This method returns a coroutine.
"""
return self.send_command(
f"/addons/{addon}/changelog", method="get", return_text=True
)
@api_data
def get_store(self):
"""Return data from the store.
@ -212,7 +221,14 @@ class HassIO:
"/supervisor/options", payload={"diagnostics": diagnostics}
)
async def send_command(self, command, method="post", payload=None, timeout=10):
async def send_command(
self,
command,
method="post",
payload=None,
timeout=10,
return_text=False,
):
"""Send API command to Hass.io.
This method is a coroutine.
@ -230,8 +246,10 @@ class HassIO:
_LOGGER.error("%s return code %d", command, request.status)
raise HassioAPIError()
answer = await request.json()
return answer
if return_text:
return await request.text(encoding="utf-8")
return await request.json()
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s request", command)

View file

@ -0,0 +1,276 @@
"""Update platform for Supervisor."""
from __future__ import annotations
from typing import Any
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import (
UpdateEntity,
UpdateEntityDescription,
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, ATTR_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import (
ADDONS_COORDINATOR,
async_update_addon,
async_update_core,
async_update_os,
async_update_supervisor,
)
from .const import (
ATTR_CHANGELOG,
ATTR_VERSION,
ATTR_VERSION_LATEST,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_OS,
DATA_KEY_SUPERVISOR,
)
from .entity import (
HassioAddonEntity,
HassioCoreEntity,
HassioOSEntity,
HassioSupervisorEntity,
)
from .handler import HassioAPIError
ENTITY_DESCRIPTION = UpdateEntityDescription(
name="Update",
key=ATTR_VERSION_LATEST,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Supervisor update based on a config entry."""
coordinator = hass.data[ADDONS_COORDINATOR]
entities = [
SupervisorSupervisorUpdateEntity(
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
),
SupervisorCoreUpdateEntity(
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
),
]
for addon in coordinator.data[DATA_KEY_ADDONS].values():
entities.append(
SupervisorAddonUpdateEntity(
addon=addon,
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
)
)
if coordinator.is_hass_os:
entities.append(
SupervisorOSUpdateEntity(
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
)
)
async_add_entities(entities)
class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
"""Update entity to handle updates for the Supervisor add-ons."""
_attr_supported_features = UpdateEntityFeature.INSTALL | UpdateEntityFeature.BACKUP
@property
def title(self) -> str | None:
"""Return the title of the update."""
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_NAME]
@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][
ATTR_VERSION_LATEST
]
@property
def current_version(self) -> str | None:
"""Version currently in use."""
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_VERSION]
@property
def release_summary(self) -> str | None:
"""Release summary for the add-on."""
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_CHANGELOG]
@property
def entity_picture(self) -> str | None:
"""Return the icon of the add-on if any."""
if not self.available:
return None
if self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_ICON]:
return f"/api/hassio/addons/{self._addon_slug}/icon"
return None
async def async_install(
self,
version: str | None = None,
backup: bool | None = False,
**kwargs: Any,
) -> None:
"""Install an update."""
try:
await async_update_addon(self.hass, slug=self._addon_slug, backup=backup)
except HassioAPIError as err:
raise HomeAssistantError(f"Error updating {self.title}: {err}") from err
else:
await self.coordinator.force_info_update_supervisor()
class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Operating System."""
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION
)
_attr_title = "Home Assistant Operating System"
@property
def latest_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION]
@property
def entity_picture(self) -> str | None:
"""Return the iconof the entity."""
return "https://brands.home-assistant.io/homeassistant/icon.png"
@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
version = AwesomeVersion(self.latest_version)
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
return "https://github.com/home-assistant/operating-system/commits/dev"
return (
f"https://github.com/home-assistant/operating-system/releases/tag/{version}"
)
async def async_install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update."""
try:
await async_update_os(self.hass, version)
except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Operating System: {err}"
) from err
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Supervisor."""
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_title = "Home Assistant Supervisor"
@property
def latest_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION]
@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
version = AwesomeVersion(self.latest_version)
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
return "https://github.com/home-assistant/supervisor/commits/main"
return f"https://github.com/home-assistant/supervisor/releases/tag/{version}"
@property
def entity_picture(self) -> str | None:
"""Return the iconof the entity."""
return "https://brands.home-assistant.io/hassio/icon.png"
async def async_install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update."""
try:
await async_update_supervisor(self.hass)
except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Supervisor: {err}"
) from err
class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
"""Update entity to handle updates for Home Assistant Core."""
_attr_supported_features = (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.BACKUP
)
_attr_title = "Home Assistant Core"
@property
def latest_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION]
@property
def entity_picture(self) -> str | None:
"""Return the iconof the entity."""
return "https://brands.home-assistant.io/homeassistant/icon.png"
@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
version = AwesomeVersion(self.latest_version)
if version.dev:
return "https://github.com/home-assistant/core/commits/dev"
return f"https://{'rc' if version.beta else 'www'}.home-assistant.io/latest-release-notes/"
async def async_install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update."""
try:
await async_update_core(self.hass, version=version, backup=backup)
except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Core {err}"
) from err

View file

@ -69,6 +69,7 @@ def mock_all(aioclient_mock, request):
"result": "ok",
"data": {
"result": "ok",
"version": "1.0.0",
"version_latest": "1.0.0",
"addons": [
{
@ -113,6 +114,8 @@ def mock_all(aioclient_mock, request):
},
},
)
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)

View file

@ -55,7 +55,7 @@ def mock_all(aioclient_mock, request):
)
aioclient_mock.get(
"http://127.0.0.1/core/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}},
json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}},
)
aioclient_mock.get(
"http://127.0.0.1/os/info",
@ -65,7 +65,7 @@ def mock_all(aioclient_mock, request):
"http://127.0.0.1/supervisor/info",
json={
"result": "ok",
"data": {"version_latest": "1.0.0"},
"data": {"version_latest": "1.0.0", "version": "1.0.0"},
"addons": [
{
"name": "test",
@ -138,6 +138,8 @@ def mock_all(aioclient_mock, request):
},
},
)
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
@ -496,12 +498,15 @@ async def test_device_registry_calls(hass):
"""Test device registry entries for hassio."""
dev_reg = async_get(hass)
supervisor_mock_data = {
"version": "1.0.0",
"version_latest": "1.0.0",
"addons": [
{
"name": "test",
"state": "started",
"slug": "test",
"installed": True,
"icon": False,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
@ -513,12 +518,13 @@ async def test_device_registry_calls(hass):
"state": "started",
"slug": "test2",
"installed": True,
"icon": False,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"url": "https://github.com",
},
]
],
}
os_mock_data = {
"board": "odroid-n2",
@ -539,21 +545,24 @@ async def test_device_registry_calls(hass):
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(dev_reg.devices) == 3
assert len(dev_reg.devices) == 5
supervisor_mock_data = {
"version": "1.0.0",
"version_latest": "1.0.0",
"addons": [
{
"name": "test2",
"state": "started",
"slug": "test2",
"installed": True,
"icon": False,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"url": "https://github.com",
},
]
],
}
# Test that when addon is removed, next update will remove the add-on and subsequent updates won't
@ -566,19 +575,22 @@ async def test_device_registry_calls(hass):
):
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1))
await hass.async_block_till_done()
assert len(dev_reg.devices) == 2
assert len(dev_reg.devices) == 4
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2))
await hass.async_block_till_done()
assert len(dev_reg.devices) == 2
assert len(dev_reg.devices) == 4
supervisor_mock_data = {
"version": "1.0.0",
"version_latest": "1.0.0",
"addons": [
{
"name": "test2",
"slug": "test2",
"state": "started",
"installed": True,
"icon": False,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
@ -589,12 +601,13 @@ async def test_device_registry_calls(hass):
"slug": "test3",
"state": "stopped",
"installed": True,
"icon": False,
"update_available": False,
"version": "1.0.0",
"version_latest": "1.0.0",
"url": "https://github.com",
},
]
],
}
# Test that when addon is added, next update will reload the entry so we register
@ -608,4 +621,4 @@ async def test_device_registry_calls(hass):
):
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
await hass.async_block_till_done()
assert len(dev_reg.devices) == 3
assert len(dev_reg.devices) == 5

View file

@ -62,6 +62,7 @@ def mock_all(aioclient_mock, request):
"result": "ok",
"data": {
"result": "ok",
"version": "1.0.0",
"version_latest": "1.0.0",
"addons": [
{
@ -106,6 +107,8 @@ def mock_all(aioclient_mock, request):
},
},
)
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)

View file

@ -0,0 +1,372 @@
"""The tests for the hassio update entities."""
import os
from unittest.mock import patch
import pytest
from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True)
def mock_all(aioclient_mock, request):
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
aioclient_mock.get(
"http://127.0.0.1/info",
json={
"result": "ok",
"data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
},
)
aioclient_mock.get(
"http://127.0.0.1/store",
json={
"result": "ok",
"data": {"addons": [], "repositories": []},
},
)
aioclient_mock.get(
"http://127.0.0.1/host/info",
json={
"result": "ok",
"data": {
"result": "ok",
"data": {
"chassis": "vm",
"operating_system": "Debian GNU/Linux 10 (buster)",
"kernel": "4.19.0-6-amd64",
},
},
},
)
aioclient_mock.get(
"http://127.0.0.1/core/info",
json={
"result": "ok",
"data": {"version_latest": "1.0.0dev222", "version": "1.0.0dev221"},
},
)
aioclient_mock.get(
"http://127.0.0.1/os/info",
json={
"result": "ok",
"data": {
"version_latest": "1.0.0dev2222",
"version": "1.0.0dev2221",
"update_available": False,
},
},
)
aioclient_mock.get(
"http://127.0.0.1/supervisor/info",
json={
"result": "ok",
"data": {
"result": "ok",
"version": "1.0.0",
"version_latest": "1.0.1dev222",
"addons": [
{
"name": "test",
"state": "started",
"slug": "test",
"installed": True,
"update_available": True,
"icon": False,
"version": "2.0.0",
"version_latest": "2.0.1",
"repository": "core",
"url": "https://github.com/home-assistant/addons/test",
},
{
"name": "test2",
"state": "stopped",
"slug": "test2",
"installed": True,
"update_available": False,
"icon": True,
"version": "3.1.0",
"version_latest": "3.1.0",
"repository": "core",
"url": "https://github.com",
},
],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.99,
"memory_usage": 182611968,
"memory_limit": 3977146368,
"memory_percent": 4.59,
"network_rx": 362570232,
"network_tx": 82374138,
"blk_read": 46010945536,
"blk_write": 15051526144,
},
},
)
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
@pytest.mark.parametrize(
"entity_id,expected",
[
("update.home_assistant_operating_system_update", "on"),
("update.home_assistant_supervisor_update", "on"),
("update.home_assistant_core_update", "on"),
("update.test_update", "on"),
("update.test2_update", "off"),
],
)
async def test_update_entities(hass, entity_id, expected, aioclient_mock):
"""Test update entities."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
# Verify that the entity have the expected state.
state = hass.states.get(entity_id)
assert state.state == expected
async def test_update_addon(hass, aioclient_mock):
"""Test updating addon update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/addons/test/update",
json={"result": "ok", "data": {}},
)
assert await hass.services.async_call(
"update",
"install",
{"entity_id": "update.test_update"},
blocking=True,
)
async def test_update_os(hass, aioclient_mock):
"""Test updating OS update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/os/update",
json={"result": "ok", "data": {}},
)
assert await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_operating_system_update"},
blocking=True,
)
async def test_update_core(hass, aioclient_mock):
"""Test updating core update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/core/update",
json={"result": "ok", "data": {}},
)
assert await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_os_update"},
blocking=True,
)
async def test_update_supervisor(hass, aioclient_mock):
"""Test updating supervisor update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/supervisor/update",
json={"result": "ok", "data": {}},
)
assert await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_supervisor_update"},
blocking=True,
)
async def test_update_addon_with_error(hass, aioclient_mock):
"""Test updating addon update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/addons/test/update",
exc=HassioAPIError,
)
with pytest.raises(HomeAssistantError):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.test_update"},
blocking=True,
)
async def test_update_os_with_error(hass, aioclient_mock):
"""Test updating OS update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/os/update",
exc=HassioAPIError,
)
with pytest.raises(HomeAssistantError):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_operating_system_update"},
blocking=True,
)
async def test_update_supervisor_with_error(hass, aioclient_mock):
"""Test updating supervisor update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/supervisor/update",
exc=HassioAPIError,
)
with pytest.raises(HomeAssistantError):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_supervisor_update"},
blocking=True,
)
async def test_update_core_with_error(hass, aioclient_mock):
"""Test updating core update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/core/update",
exc=HassioAPIError,
)
with pytest.raises(HomeAssistantError):
assert not await hass.services.async_call(
"update",
"install",
{"entity_id": "update.home_assistant_core_update"},
blocking=True,
)