Add update platform to the Supervisor integration (#68475)
This commit is contained in:
parent
1b955970f8
commit
d17f8e9ed6
10 changed files with 901 additions and 19 deletions
|
@ -51,6 +51,7 @@ from .auth import async_setup_auth_view
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ADDON,
|
ATTR_ADDON,
|
||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
|
ATTR_CHANGELOG,
|
||||||
ATTR_DISCOVERY,
|
ATTR_DISCOVERY,
|
||||||
ATTR_FOLDERS,
|
ATTR_FOLDERS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
|
@ -63,6 +64,9 @@ from .const import (
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
DATA_KEY_ADDONS,
|
DATA_KEY_ADDONS,
|
||||||
|
DATA_KEY_CORE,
|
||||||
|
DATA_KEY_OS,
|
||||||
|
DATA_KEY_SUPERVISOR,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SupervisorEntityModel,
|
SupervisorEntityModel,
|
||||||
)
|
)
|
||||||
|
@ -77,7 +81,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE]
|
||||||
|
|
||||||
CONF_FRONTEND_REPO = "development_repo"
|
CONF_FRONTEND_REPO = "development_repo"
|
||||||
|
|
||||||
|
@ -93,6 +97,7 @@ DATA_STORE = "hassio_store"
|
||||||
DATA_INFO = "hassio_info"
|
DATA_INFO = "hassio_info"
|
||||||
DATA_OS_INFO = "hassio_os_info"
|
DATA_OS_INFO = "hassio_os_info"
|
||||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||||
|
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||||
|
|
||||||
|
@ -239,14 +244,22 @@ async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
@api_data
|
@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.
|
"""Update add-on.
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
The caller of the function should handle HassioAPIError.
|
||||||
"""
|
"""
|
||||||
hassio = hass.data[DOMAIN]
|
hassio = hass.data[DOMAIN]
|
||||||
command = f"/addons/{slug}/update"
|
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
|
@bind_hass
|
||||||
|
@ -323,6 +336,52 @@ async def async_create_backup(
|
||||||
return await hassio.send_command(command, payload=payload, timeout=None)
|
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
|
@callback
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def get_info(hass):
|
def get_info(hass):
|
||||||
|
@ -373,6 +432,16 @@ def get_addons_stats(hass):
|
||||||
return hass.data.get(DATA_ADDONS_STATS)
|
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
|
@callback
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def get_os_info(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)
|
stats = await hassio.get_addon_stats(slug)
|
||||||
return (slug, stats)
|
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):
|
async def update_info_data(now):
|
||||||
"""Update last available supervisor information."""
|
"""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]
|
*[update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
|
||||||
)
|
)
|
||||||
hass.data[DATA_ADDONS_STATS] = dict(stats_data)
|
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:
|
if ADDONS_COORDINATOR in hass.data:
|
||||||
await hass.data[ADDONS_COORDINATOR].async_refresh()
|
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)
|
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
|
@callback
|
||||||
def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None:
|
def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None:
|
||||||
"""Remove addons from the device registry."""
|
"""Remove addons from the device registry."""
|
||||||
|
@ -720,6 +835,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_method=self._async_update_data,
|
update_method=self._async_update_data,
|
||||||
)
|
)
|
||||||
|
self.hassio: HassIO = hass.data[DOMAIN]
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.entry_id = config_entry.entry_id
|
self.entry_id = config_entry.entry_id
|
||||||
self.dev_reg = dev_reg
|
self.dev_reg = dev_reg
|
||||||
|
@ -730,6 +846,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
supervisor_info = get_supervisor_info(self.hass)
|
supervisor_info = get_supervisor_info(self.hass)
|
||||||
addons_stats = get_addons_stats(self.hass)
|
addons_stats = get_addons_stats(self.hass)
|
||||||
|
addons_changelogs = get_addons_changelogs(self.hass)
|
||||||
store_data = get_store(self.hass)
|
store_data = get_store(self.hass)
|
||||||
|
|
||||||
repositories = {
|
repositories = {
|
||||||
|
@ -741,6 +858,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
addon[ATTR_SLUG]: {
|
addon[ATTR_SLUG]: {
|
||||||
**addon,
|
**addon,
|
||||||
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
|
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
|
||||||
|
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
|
||||||
ATTR_REPOSITORY: repositories.get(
|
ATTR_REPOSITORY: repositories.get(
|
||||||
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
||||||
),
|
),
|
||||||
|
@ -748,16 +866,25 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
for addon in supervisor_info.get("addons", [])
|
for addon in supervisor_info.get("addons", [])
|
||||||
}
|
}
|
||||||
if self.is_hass_os:
|
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 this is the initial refresh, register all addons and return the dict
|
||||||
if not self.data:
|
if not self.data:
|
||||||
async_register_addons_in_dev_reg(
|
async_register_addons_in_dev_reg(
|
||||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
|
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:
|
if self.is_hass_os:
|
||||||
async_register_os_in_dev_reg(
|
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
|
# Remove add-ons that are no longer installed from device registry
|
||||||
|
@ -782,3 +909,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return new_data
|
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()
|
||||||
|
|
|
@ -32,6 +32,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
|
|
||||||
COMMON_ENTITY_DESCRIPTIONS = (
|
COMMON_ENTITY_DESCRIPTIONS = (
|
||||||
HassioBinarySensorEntityDescription(
|
HassioBinarySensorEntityDescription(
|
||||||
|
# Deprecated, scheduled to be removed in 2022.6
|
||||||
device_class=BinarySensorDeviceClass.UPDATE,
|
device_class=BinarySensorDeviceClass.UPDATE,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
key=ATTR_UPDATE_AVAILABLE,
|
key=ATTR_UPDATE_AVAILABLE,
|
||||||
|
|
|
@ -43,6 +43,7 @@ ATTR_VERSION = "version"
|
||||||
ATTR_VERSION_LATEST = "version_latest"
|
ATTR_VERSION_LATEST = "version_latest"
|
||||||
ATTR_UPDATE_AVAILABLE = "update_available"
|
ATTR_UPDATE_AVAILABLE = "update_available"
|
||||||
ATTR_CPU_PERCENT = "cpu_percent"
|
ATTR_CPU_PERCENT = "cpu_percent"
|
||||||
|
ATTR_CHANGELOG = "changelog"
|
||||||
ATTR_MEMORY_PERCENT = "memory_percent"
|
ATTR_MEMORY_PERCENT = "memory_percent"
|
||||||
ATTR_SLUG = "slug"
|
ATTR_SLUG = "slug"
|
||||||
ATTR_STATE = "state"
|
ATTR_STATE = "state"
|
||||||
|
@ -53,6 +54,8 @@ ATTR_REPOSITORY = "repository"
|
||||||
|
|
||||||
DATA_KEY_ADDONS = "addons"
|
DATA_KEY_ADDONS = "addons"
|
||||||
DATA_KEY_OS = "os"
|
DATA_KEY_OS = "os"
|
||||||
|
DATA_KEY_SUPERVISOR = "supervisor"
|
||||||
|
DATA_KEY_CORE = "core"
|
||||||
|
|
||||||
|
|
||||||
class SupervisorEntityModel(str, Enum):
|
class SupervisorEntityModel(str, Enum):
|
||||||
|
@ -60,3 +63,5 @@ class SupervisorEntityModel(str, Enum):
|
||||||
|
|
||||||
ADDON = "Home Assistant Add-on"
|
ADDON = "Home Assistant Add-on"
|
||||||
OS = "Home Assistant Operating System"
|
OS = "Home Assistant Operating System"
|
||||||
|
CORE = "Home Assistant Core"
|
||||||
|
SUPERVIOSR = "Home Assistant Supervisor"
|
||||||
|
|
|
@ -8,7 +8,13 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import DOMAIN, HassioDataUpdateCoordinator
|
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]):
|
class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||||
|
@ -33,8 +39,9 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return (
|
return (
|
||||||
super().available
|
super().available
|
||||||
|
and DATA_KEY_ADDONS in self.coordinator.data
|
||||||
and self.entity_description.key
|
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 True if entity is available."""
|
||||||
return (
|
return (
|
||||||
super().available
|
super().available
|
||||||
|
and DATA_KEY_OS in self.coordinator.data
|
||||||
and self.entity_description.key in self.coordinator.data[DATA_KEY_OS]
|
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]
|
||||||
|
)
|
||||||
|
|
|
@ -127,6 +127,15 @@ class HassIO:
|
||||||
"""
|
"""
|
||||||
return self.send_command(f"/addons/{addon}/stats", method="get")
|
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
|
@api_data
|
||||||
def get_store(self):
|
def get_store(self):
|
||||||
"""Return data from the store.
|
"""Return data from the store.
|
||||||
|
@ -212,7 +221,14 @@ class HassIO:
|
||||||
"/supervisor/options", payload={"diagnostics": diagnostics}
|
"/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.
|
"""Send API command to Hass.io.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
|
@ -230,8 +246,10 @@ class HassIO:
|
||||||
_LOGGER.error("%s return code %d", command, request.status)
|
_LOGGER.error("%s return code %d", command, request.status)
|
||||||
raise HassioAPIError()
|
raise HassioAPIError()
|
||||||
|
|
||||||
answer = await request.json()
|
if return_text:
|
||||||
return answer
|
return await request.text(encoding="utf-8")
|
||||||
|
|
||||||
|
return await request.json()
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error("Timeout on %s request", command)
|
_LOGGER.error("Timeout on %s request", command)
|
||||||
|
|
276
homeassistant/components/hassio/update.py
Normal file
276
homeassistant/components/hassio/update.py
Normal 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
|
|
@ -69,6 +69,7 @@ def mock_all(aioclient_mock, request):
|
||||||
"result": "ok",
|
"result": "ok",
|
||||||
"data": {
|
"data": {
|
||||||
"result": "ok",
|
"result": "ok",
|
||||||
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"addons": [
|
"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(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,7 +55,7 @@ def mock_all(aioclient_mock, request):
|
||||||
)
|
)
|
||||||
aioclient_mock.get(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/core/info",
|
"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(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/os/info",
|
"http://127.0.0.1/os/info",
|
||||||
|
@ -65,7 +65,7 @@ def mock_all(aioclient_mock, request):
|
||||||
"http://127.0.0.1/supervisor/info",
|
"http://127.0.0.1/supervisor/info",
|
||||||
json={
|
json={
|
||||||
"result": "ok",
|
"result": "ok",
|
||||||
"data": {"version_latest": "1.0.0"},
|
"data": {"version_latest": "1.0.0", "version": "1.0.0"},
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "test",
|
"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(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
"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."""
|
"""Test device registry entries for hassio."""
|
||||||
dev_reg = async_get(hass)
|
dev_reg = async_get(hass)
|
||||||
supervisor_mock_data = {
|
supervisor_mock_data = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"state": "started",
|
"state": "started",
|
||||||
"slug": "test",
|
"slug": "test",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
|
"icon": False,
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
|
@ -513,12 +518,13 @@ async def test_device_registry_calls(hass):
|
||||||
"state": "started",
|
"state": "started",
|
||||||
"slug": "test2",
|
"slug": "test2",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
|
"icon": False,
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"url": "https://github.com",
|
"url": "https://github.com",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
os_mock_data = {
|
os_mock_data = {
|
||||||
"board": "odroid-n2",
|
"board": "odroid-n2",
|
||||||
|
@ -539,21 +545,24 @@ async def test_device_registry_calls(hass):
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(dev_reg.devices) == 3
|
assert len(dev_reg.devices) == 5
|
||||||
|
|
||||||
supervisor_mock_data = {
|
supervisor_mock_data = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "test2",
|
"name": "test2",
|
||||||
"state": "started",
|
"state": "started",
|
||||||
"slug": "test2",
|
"slug": "test2",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
|
"icon": False,
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"url": "https://github.com",
|
"url": "https://github.com",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test that when addon is removed, next update will remove the add-on and subsequent updates won't
|
# 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))
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1))
|
||||||
await hass.async_block_till_done()
|
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))
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(dev_reg.devices) == 2
|
assert len(dev_reg.devices) == 4
|
||||||
|
|
||||||
supervisor_mock_data = {
|
supervisor_mock_data = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "test2",
|
"name": "test2",
|
||||||
"slug": "test2",
|
"slug": "test2",
|
||||||
"state": "started",
|
"state": "started",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
|
"icon": False,
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
|
@ -589,12 +601,13 @@ async def test_device_registry_calls(hass):
|
||||||
"slug": "test3",
|
"slug": "test3",
|
||||||
"state": "stopped",
|
"state": "stopped",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
|
"icon": False,
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"url": "https://github.com",
|
"url": "https://github.com",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test that when addon is added, next update will reload the entry so we register
|
# 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))
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(dev_reg.devices) == 3
|
assert len(dev_reg.devices) == 5
|
||||||
|
|
|
@ -62,6 +62,7 @@ def mock_all(aioclient_mock, request):
|
||||||
"result": "ok",
|
"result": "ok",
|
||||||
"data": {
|
"data": {
|
||||||
"result": "ok",
|
"result": "ok",
|
||||||
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"addons": [
|
"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(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||||
)
|
)
|
||||||
|
|
372
tests/components/hassio/test_update.py
Normal file
372
tests/components/hassio/test_update.py
Normal 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,
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue