Add hassio addon_update service and hassio config entry with addon and OS devices and entities (#46342)
* add addon_update service, use config flow to set up config entry, create disabled sensors * move most of entity logic to common entity class, improve device info, get rid of config_flow user step * fix setup logic * additional refactor * fix refactored logic * fix config flow tests * add test for addon_update service and get_addons_info * add entry setup and unload test and fix update coordinator * handle if entry setup calls unload * return nothing for coordinator if entry is being reloaded because coordinator will get recreated anyway * remove entry when HA instance is no longer hassio and add corresponding test * handle adding and removing device registry entries * better config entry reload logic * fix comment * bugfix * fix flake error * switch pass to return * use repository attribute for model and fallback to url * use custom 'system' source since hassio source is misleading * Update homeassistant/components/hassio/entity.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * update remove addons function name * Update homeassistant/components/hassio/__init__.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * fix import * pop coordinator after unload * additional fixes * always pass in sensor name when creating entity * prefix one more function with async and fix tests * use supervisor info for addons since list is already filtered on what's installed * remove unused service * update sensor names * remove added handler function * use walrus * add OS device and sensors * fix * re-add addon_update service schema * add more test coverage and exclude entities from tests * check if instance is using hass OS in order to create OS entities Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
d2db58d138
commit
0592309b65
10 changed files with 661 additions and 10 deletions
|
@ -377,6 +377,9 @@ omit =
|
||||||
homeassistant/components/harmony/data.py
|
homeassistant/components/harmony/data.py
|
||||||
homeassistant/components/harmony/remote.py
|
homeassistant/components/harmony/remote.py
|
||||||
homeassistant/components/harmony/util.py
|
homeassistant/components/harmony/util.py
|
||||||
|
homeassistant/components/hassio/binary_sensor.py
|
||||||
|
homeassistant/components/hassio/entity.py
|
||||||
|
homeassistant/components/hassio/sensor.py
|
||||||
homeassistant/components/haveibeenpwned/sensor.py
|
homeassistant/components/haveibeenpwned/sensor.py
|
||||||
homeassistant/components/hdmi_cec/*
|
homeassistant/components/hdmi_cec/*
|
||||||
homeassistant/components/heatmiser/climate.py
|
homeassistant/components/heatmiser/climate.py
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
"""Support for Hass.io."""
|
"""Support for Hass.io."""
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
|
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
|
||||||
import homeassistant.config as conf_util
|
import homeassistant.config as conf_util
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
|
ATTR_SERVICE,
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
EVENT_CORE_CONFIG_UPDATE,
|
||||||
SERVICE_HOMEASSISTANT_RESTART,
|
SERVICE_HOMEASSISTANT_RESTART,
|
||||||
SERVICE_HOMEASSISTANT_STOP,
|
SERVICE_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
|
from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
@ -32,7 +37,11 @@ from .const import (
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
ATTR_PASSWORD,
|
ATTR_PASSWORD,
|
||||||
|
ATTR_REPOSITORY,
|
||||||
|
ATTR_SLUG,
|
||||||
ATTR_SNAPSHOT,
|
ATTR_SNAPSHOT,
|
||||||
|
ATTR_URL,
|
||||||
|
ATTR_VERSION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .discovery import async_setup_discovery_view
|
from .discovery import async_setup_discovery_view
|
||||||
|
@ -46,6 +55,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
|
PLATFORMS = ["binary_sensor", "sensor"]
|
||||||
|
|
||||||
CONF_FRONTEND_REPO = "development_repo"
|
CONF_FRONTEND_REPO = "development_repo"
|
||||||
|
|
||||||
|
@ -62,9 +72,12 @@ DATA_OS_INFO = "hassio_os_info"
|
||||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
|
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
|
||||||
|
|
||||||
|
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||||
|
|
||||||
SERVICE_ADDON_START = "addon_start"
|
SERVICE_ADDON_START = "addon_start"
|
||||||
SERVICE_ADDON_STOP = "addon_stop"
|
SERVICE_ADDON_STOP = "addon_stop"
|
||||||
SERVICE_ADDON_RESTART = "addon_restart"
|
SERVICE_ADDON_RESTART = "addon_restart"
|
||||||
|
SERVICE_ADDON_UPDATE = "addon_update"
|
||||||
SERVICE_ADDON_STDIN = "addon_stdin"
|
SERVICE_ADDON_STDIN = "addon_stdin"
|
||||||
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
||||||
SERVICE_HOST_REBOOT = "host_reboot"
|
SERVICE_HOST_REBOOT = "host_reboot"
|
||||||
|
@ -110,6 +123,7 @@ MAP_SERVICE_API = {
|
||||||
SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False),
|
SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False),
|
||||||
SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False),
|
SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False),
|
||||||
SERVICE_ADDON_RESTART: ("/addons/{addon}/restart", SCHEMA_ADDON, 60, False),
|
SERVICE_ADDON_RESTART: ("/addons/{addon}/restart", SCHEMA_ADDON, 60, False),
|
||||||
|
SERVICE_ADDON_UPDATE: ("/addons/{addon}/update", SCHEMA_ADDON, 60, False),
|
||||||
SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False),
|
SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False),
|
||||||
SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False),
|
SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False),
|
||||||
SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False),
|
SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False),
|
||||||
|
@ -286,13 +300,17 @@ def get_supervisor_ip():
|
||||||
return os.environ["SUPERVISOR"].partition(":")[0]
|
return os.environ["SUPERVISOR"].partition(":")[0]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||||
"""Set up the Hass.io component."""
|
"""Set up the Hass.io component."""
|
||||||
# Check local setup
|
# Check local setup
|
||||||
for env in ("HASSIO", "HASSIO_TOKEN"):
|
for env in ("HASSIO", "HASSIO_TOKEN"):
|
||||||
if os.environ.get(env):
|
if os.environ.get(env):
|
||||||
continue
|
continue
|
||||||
_LOGGER.error("Missing %s environment variable", env)
|
_LOGGER.error("Missing %s environment variable", env)
|
||||||
|
if config_entries := hass.config_entries.async_entries(DOMAIN):
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_remove(config_entries[0].entry_id)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async_load_websocket_api(hass)
|
async_load_websocket_api(hass)
|
||||||
|
@ -402,6 +420,8 @@ async def async_setup(hass, config):
|
||||||
hass.data[DATA_CORE_INFO] = await hassio.get_core_info()
|
hass.data[DATA_CORE_INFO] = await hassio.get_core_info()
|
||||||
hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info()
|
hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info()
|
||||||
hass.data[DATA_OS_INFO] = await hassio.get_os_info()
|
hass.data[DATA_OS_INFO] = await hassio.get_os_info()
|
||||||
|
if ADDONS_COORDINATOR in hass.data:
|
||||||
|
await hass.data[ADDONS_COORDINATOR].async_refresh()
|
||||||
except HassioAPIError as err:
|
except HassioAPIError as err:
|
||||||
_LOGGER.warning("Can't read last version: %s", err)
|
_LOGGER.warning("Can't read last version: %s", err)
|
||||||
|
|
||||||
|
@ -455,4 +475,143 @@ async def async_setup(hass, config):
|
||||||
# Init add-on ingress panels
|
# Init add-on ingress panels
|
||||||
await async_setup_addon_panel(hass, hassio)
|
await async_setup_addon_panel(hass, hassio)
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
dev_reg = await async_get_registry(hass)
|
||||||
|
coordinator = HassioDataUpdateCoordinator(hass, config_entry, dev_reg)
|
||||||
|
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistantType, config_entry: ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||||
|
for platform in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pop add-on data
|
||||||
|
hass.data.pop(ADDONS_COORDINATOR, None)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_register_addons_in_dev_reg(
|
||||||
|
entry_id: str, dev_reg: DeviceRegistry, addons: List[Dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
|
"""Register addons in the device registry."""
|
||||||
|
for addon in addons:
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry_id,
|
||||||
|
identifiers={(DOMAIN, addon[ATTR_SLUG])},
|
||||||
|
manufacturer=addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL) or "unknown",
|
||||||
|
model="Home Assistant Add-on",
|
||||||
|
sw_version=addon[ATTR_VERSION],
|
||||||
|
name=addon[ATTR_NAME],
|
||||||
|
entry_type=ATTR_SERVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_register_os_in_dev_reg(
|
||||||
|
entry_id: str, dev_reg: DeviceRegistry, os_dict: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Register OS in the device registry."""
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry_id,
|
||||||
|
identifiers={(DOMAIN, "OS")},
|
||||||
|
manufacturer="Home Assistant",
|
||||||
|
model="Home Assistant Operating System",
|
||||||
|
sw_version=os_dict[ATTR_VERSION],
|
||||||
|
name="Home Assistant Operating System",
|
||||||
|
entry_type=ATTR_SERVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove_addons_from_dev_reg(
|
||||||
|
dev_reg: DeviceRegistry, addons: List[Dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
|
"""Remove addons from the device registry."""
|
||||||
|
for addon_slug in addons:
|
||||||
|
if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}):
|
||||||
|
dev_reg.async_remove_device(dev.id)
|
||||||
|
|
||||||
|
|
||||||
|
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Class to retrieve Hass.io status."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: DeviceRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_method=self._async_update_data,
|
||||||
|
)
|
||||||
|
self.data = {}
|
||||||
|
self.entry_id = config_entry.entry_id
|
||||||
|
self.dev_reg = dev_reg
|
||||||
|
self.is_hass_os = "hassos" in get_info(self.hass)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
|
"""Update data via library."""
|
||||||
|
new_data = {}
|
||||||
|
addon_data = get_supervisor_info(self.hass)
|
||||||
|
|
||||||
|
new_data["addons"] = {
|
||||||
|
addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", [])
|
||||||
|
}
|
||||||
|
if self.is_hass_os:
|
||||||
|
new_data["os"] = get_os_info(self.hass)
|
||||||
|
|
||||||
|
# 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["addons"].values()
|
||||||
|
)
|
||||||
|
if self.is_hass_os:
|
||||||
|
async_register_os_in_dev_reg(
|
||||||
|
self.entry_id, self.dev_reg, new_data["os"]
|
||||||
|
)
|
||||||
|
return new_data
|
||||||
|
|
||||||
|
# Remove add-ons that are no longer installed from device registry
|
||||||
|
if removed_addons := list(
|
||||||
|
set(self.data["addons"].keys()) - set(new_data["addons"].keys())
|
||||||
|
):
|
||||||
|
async_remove_addons_from_dev_reg(self.dev_reg, removed_addons)
|
||||||
|
|
||||||
|
# If there are new add-ons, we should reload the config entry so we can
|
||||||
|
# create new devices and entities. We can return an empty dict because
|
||||||
|
# coordinator will be recreated.
|
||||||
|
if list(set(new_data["addons"].keys()) - set(self.data["addons"].keys())):
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(self.entry_id)
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return new_data
|
||||||
|
|
50
homeassistant/components/hassio/binary_sensor.py
Normal file
50
homeassistant/components/hassio/binary_sensor.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""Binary sensor platform for Hass.io addons."""
|
||||||
|
from typing import Callable, List
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from . import ADDONS_COORDINATOR
|
||||||
|
from .const import ATTR_UPDATE_AVAILABLE
|
||||||
|
from .entity import HassioAddonEntity, HassioOSEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], bool], None],
|
||||||
|
) -> None:
|
||||||
|
"""Binary sensor set up for Hass.io config entry."""
|
||||||
|
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
HassioAddonBinarySensor(
|
||||||
|
coordinator, addon, ATTR_UPDATE_AVAILABLE, "Update Available"
|
||||||
|
)
|
||||||
|
for addon in coordinator.data.values()
|
||||||
|
]
|
||||||
|
if coordinator.is_hass_os:
|
||||||
|
entities.append(
|
||||||
|
HassioOSBinarySensor(coordinator, ATTR_UPDATE_AVAILABLE, "Update Available")
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity):
|
||||||
|
"""Binary sensor to track whether an update is available for a Hass.io add-on."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.addon_info[self.attribute_name]
|
||||||
|
|
||||||
|
|
||||||
|
class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity):
|
||||||
|
"""Binary sensor to track whether an update is available for Hass.io OS."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.os_info[self.attribute_name]
|
22
homeassistant/components/hassio/config_flow.py
Normal file
22
homeassistant/components/hassio/config_flow.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
"""Config flow for Home Assistant Supervisor integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
|
||||||
|
from . import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Home Assistant Supervisor."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
|
|
||||||
|
async def async_step_system(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
# We only need one Hass.io config entry
|
||||||
|
await self.async_set_unique_id(DOMAIN)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=DOMAIN.title(), data={})
|
|
@ -29,7 +29,6 @@ X_INGRESS_PATH = "X-Ingress-Path"
|
||||||
X_HASS_USER_ID = "X-Hass-User-ID"
|
X_HASS_USER_ID = "X-Hass-User-ID"
|
||||||
X_HASS_IS_ADMIN = "X-Hass-Is-Admin"
|
X_HASS_IS_ADMIN = "X-Hass-Is-Admin"
|
||||||
|
|
||||||
|
|
||||||
WS_TYPE = "type"
|
WS_TYPE = "type"
|
||||||
WS_ID = "id"
|
WS_ID = "id"
|
||||||
|
|
||||||
|
@ -38,3 +37,11 @@ WS_TYPE_EVENT = "supervisor/event"
|
||||||
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||||
|
|
||||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||||
|
|
||||||
|
# Add-on keys
|
||||||
|
ATTR_VERSION = "version"
|
||||||
|
ATTR_VERSION_LATEST = "version_latest"
|
||||||
|
ATTR_UPDATE_AVAILABLE = "update_available"
|
||||||
|
ATTR_SLUG = "slug"
|
||||||
|
ATTR_URL = "url"
|
||||||
|
ATTR_REPOSITORY = "repository"
|
||||||
|
|
93
homeassistant/components/hassio/entity.py
Normal file
93
homeassistant/components/hassio/entity.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
"""Base for Hass.io entities."""
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_NAME
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import DOMAIN, HassioDataUpdateCoordinator
|
||||||
|
from .const import ATTR_SLUG
|
||||||
|
|
||||||
|
|
||||||
|
class HassioAddonEntity(CoordinatorEntity):
|
||||||
|
"""Base entity for a Hass.io add-on."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: HassioDataUpdateCoordinator,
|
||||||
|
addon: Dict[str, Any],
|
||||||
|
attribute_name: str,
|
||||||
|
sensor_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize base entity."""
|
||||||
|
self.addon_slug = addon[ATTR_SLUG]
|
||||||
|
self.addon_name = addon[ATTR_NAME]
|
||||||
|
self._data_key = "addons"
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
self.sensor_name = sensor_name
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def addon_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return add-on info."""
|
||||||
|
return self.coordinator.data[self._data_key][self.addon_slug]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return entity name."""
|
||||||
|
return f"{self.addon_name}: {self.sensor_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return unique ID for entity."""
|
||||||
|
return f"{self.addon_slug}_{self.attribute_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return {"identifiers": {(DOMAIN, self.addon_slug)}}
|
||||||
|
|
||||||
|
|
||||||
|
class HassioOSEntity(CoordinatorEntity):
|
||||||
|
"""Base Entity for Hass.io OS."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: HassioDataUpdateCoordinator,
|
||||||
|
attribute_name: str,
|
||||||
|
sensor_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize base entity."""
|
||||||
|
self._data_key = "os"
|
||||||
|
self.attribute_name = attribute_name
|
||||||
|
self.sensor_name = sensor_name
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def os_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return OS info."""
|
||||||
|
return self.coordinator.data[self._data_key]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return entity name."""
|
||||||
|
return f"Home Assistant Operating System: {self.sensor_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return unique ID for entity."""
|
||||||
|
return f"home_assistant_os_{self.attribute_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Dict[str, Any]:
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return {"identifiers": {(DOMAIN, "OS")}}
|
52
homeassistant/components/hassio/sensor.py
Normal file
52
homeassistant/components/hassio/sensor.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
"""Sensor platform for Hass.io addons."""
|
||||||
|
from typing import Callable, List
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from . import ADDONS_COORDINATOR
|
||||||
|
from .const import ATTR_VERSION, ATTR_VERSION_LATEST
|
||||||
|
from .entity import HassioAddonEntity, HassioOSEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], bool], None],
|
||||||
|
) -> None:
|
||||||
|
"""Sensor set up for Hass.io config entry."""
|
||||||
|
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for attribute_name, sensor_name in (
|
||||||
|
(ATTR_VERSION, "Version"),
|
||||||
|
(ATTR_VERSION_LATEST, "Newest Version"),
|
||||||
|
):
|
||||||
|
for addon in coordinator.data.values():
|
||||||
|
entities.append(
|
||||||
|
HassioAddonSensor(coordinator, addon, attribute_name, sensor_name)
|
||||||
|
)
|
||||||
|
if coordinator.is_hass_os:
|
||||||
|
entities.append(HassioOSSensor(coordinator, attribute_name, sensor_name))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class HassioAddonSensor(HassioAddonEntity):
|
||||||
|
"""Sensor to track a Hass.io add-on attribute."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return state of entity."""
|
||||||
|
return self.addon_info[self.attribute_name]
|
||||||
|
|
||||||
|
|
||||||
|
class HassioOSSensor(HassioOSEntity):
|
||||||
|
"""Sensor to track a Hass.io add-on attribute."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return state of entity."""
|
||||||
|
return self.os_info[self.attribute_name]
|
|
@ -46,6 +46,18 @@ addon_stop:
|
||||||
selector:
|
selector:
|
||||||
addon:
|
addon:
|
||||||
|
|
||||||
|
addon_update:
|
||||||
|
name: Update add-on.
|
||||||
|
description: Update add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.
|
||||||
|
fields:
|
||||||
|
addon:
|
||||||
|
name: Add-on
|
||||||
|
required: true
|
||||||
|
description: The add-on slug.
|
||||||
|
example: core_ssh
|
||||||
|
selector:
|
||||||
|
addon:
|
||||||
|
|
||||||
host_reboot:
|
host_reboot:
|
||||||
name: Reboot the host system.
|
name: Reboot the host system.
|
||||||
description: Reboot the host system.
|
description: Reboot the host system.
|
||||||
|
|
36
tests/components/hassio/test_config_flow.py
Normal file
36
tests/components/hassio/test_config_flow.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""Test the Home Assistant Supervisor config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import setup
|
||||||
|
from homeassistant.components.hassio import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow(hass):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.hassio.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "system"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == DOMAIN.title()
|
||||||
|
assert result["data"] == {}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multiple_entries(hass):
|
||||||
|
"""Test creating multiple hassio entries."""
|
||||||
|
await test_config_flow(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "system"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_configured"
|
|
@ -1,17 +1,91 @@
|
||||||
"""The tests for the hassio component."""
|
"""The tests for the hassio component."""
|
||||||
|
from datetime import timedelta
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
from homeassistant.components.hassio import STORAGE_KEY
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
|
from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.helpers.device_registry import async_get
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import mock_all # noqa
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"}
|
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/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.0"}},
|
||||||
|
)
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/os/info",
|
||||||
|
json={"result": "ok", "data": {"version_latest": "1.0.0"}},
|
||||||
|
)
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/supervisor/info",
|
||||||
|
json={
|
||||||
|
"result": "ok",
|
||||||
|
"data": {"version_latest": "1.0.0"},
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"slug": "test",
|
||||||
|
"installed": True,
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"url": "https://github.com/home-assistant/addons/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test2",
|
||||||
|
"slug": "test2",
|
||||||
|
"installed": True,
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"url": "https://github.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_api_ping(hass, aioclient_mock):
|
async def test_setup_api_ping(hass, aioclient_mock):
|
||||||
"""Test setup with API ping."""
|
"""Test setup with API ping."""
|
||||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||||
|
@ -193,6 +267,7 @@ async def test_service_register(hassio_env, hass):
|
||||||
assert hass.services.has_service("hassio", "addon_start")
|
assert hass.services.has_service("hassio", "addon_start")
|
||||||
assert hass.services.has_service("hassio", "addon_stop")
|
assert hass.services.has_service("hassio", "addon_stop")
|
||||||
assert hass.services.has_service("hassio", "addon_restart")
|
assert hass.services.has_service("hassio", "addon_restart")
|
||||||
|
assert hass.services.has_service("hassio", "addon_update")
|
||||||
assert hass.services.has_service("hassio", "addon_stdin")
|
assert hass.services.has_service("hassio", "addon_stdin")
|
||||||
assert hass.services.has_service("hassio", "host_shutdown")
|
assert hass.services.has_service("hassio", "host_shutdown")
|
||||||
assert hass.services.has_service("hassio", "host_reboot")
|
assert hass.services.has_service("hassio", "host_reboot")
|
||||||
|
@ -210,6 +285,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/restart", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/restart", json={"result": "ok"})
|
||||||
|
aioclient_mock.post("http://127.0.0.1/addons/test/update", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"})
|
||||||
|
@ -225,19 +301,20 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||||
await hass.services.async_call("hassio", "addon_start", {"addon": "test"})
|
await hass.services.async_call("hassio", "addon_start", {"addon": "test"})
|
||||||
await hass.services.async_call("hassio", "addon_stop", {"addon": "test"})
|
await hass.services.async_call("hassio", "addon_stop", {"addon": "test"})
|
||||||
await hass.services.async_call("hassio", "addon_restart", {"addon": "test"})
|
await hass.services.async_call("hassio", "addon_restart", {"addon": "test"})
|
||||||
|
await hass.services.async_call("hassio", "addon_update", {"addon": "test"})
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"hassio", "addon_stdin", {"addon": "test", "input": "test"}
|
"hassio", "addon_stdin", {"addon": "test", "input": "test"}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 7
|
assert aioclient_mock.call_count == 8
|
||||||
assert aioclient_mock.mock_calls[-1][2] == "test"
|
assert aioclient_mock.mock_calls[-1][2] == "test"
|
||||||
|
|
||||||
await hass.services.async_call("hassio", "host_shutdown", {})
|
await hass.services.async_call("hassio", "host_shutdown", {})
|
||||||
await hass.services.async_call("hassio", "host_reboot", {})
|
await hass.services.async_call("hassio", "host_reboot", {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 9
|
assert aioclient_mock.call_count == 10
|
||||||
|
|
||||||
await hass.services.async_call("hassio", "snapshot_full", {})
|
await hass.services.async_call("hassio", "snapshot_full", {})
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -247,7 +324,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 11
|
assert aioclient_mock.call_count == 12
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"addons": ["test"],
|
"addons": ["test"],
|
||||||
"folders": ["ssl"],
|
"folders": ["ssl"],
|
||||||
|
@ -268,7 +345,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 13
|
assert aioclient_mock.call_count == 14
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"addons": ["test"],
|
"addons": ["test"],
|
||||||
"folders": ["ssl"],
|
"folders": ["ssl"],
|
||||||
|
@ -302,3 +379,143 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
||||||
assert mock_check_config.called
|
assert mock_check_config.called
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 5
|
assert aioclient_mock.call_count == 5
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entry_load_and_unload(hass):
|
||||||
|
"""Test loading and unloading config entry."""
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert SENSOR_DOMAIN in hass.config.components
|
||||||
|
assert BINARY_SENSOR_DOMAIN in hass.config.components
|
||||||
|
assert ADDONS_COORDINATOR in hass.data
|
||||||
|
|
||||||
|
assert await config_entry.async_unload(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert ADDONS_COORDINATOR not in hass.data
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_off_hassio(hass):
|
||||||
|
"""Test that when a user moves instance off Hass.io, config entry gets cleaned up."""
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.config_entries.async_entries(DOMAIN) == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_registry_calls(hass):
|
||||||
|
"""Test device registry entries for hassio."""
|
||||||
|
dev_reg = async_get(hass)
|
||||||
|
supervisor_mock_data = {
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"slug": "test",
|
||||||
|
"installed": True,
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"repository": "test",
|
||||||
|
"url": "https://github.com/home-assistant/addons/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test2",
|
||||||
|
"slug": "test2",
|
||||||
|
"installed": True,
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"url": "https://github.com",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
os_mock_data = {
|
||||||
|
"board": "odroid-n2",
|
||||||
|
"boot": "A",
|
||||||
|
"update_available": False,
|
||||||
|
"version": "5.12",
|
||||||
|
"version_latest": "5.12",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
||||||
|
return_value=supervisor_mock_data,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_os_info",
|
||||||
|
return_value=os_mock_data,
|
||||||
|
):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||||
|
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
|
||||||
|
|
||||||
|
supervisor_mock_data = {
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "test2",
|
||||||
|
"slug": "test2",
|
||||||
|
"installed": True,
|
||||||
|
"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
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
||||||
|
return_value=supervisor_mock_data,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_os_info",
|
||||||
|
return_value=os_mock_data,
|
||||||
|
):
|
||||||
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(dev_reg.devices) == 2
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(dev_reg.devices) == 2
|
||||||
|
|
||||||
|
supervisor_mock_data = {
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "test2",
|
||||||
|
"slug": "test2",
|
||||||
|
"installed": True,
|
||||||
|
"update_available": False,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_latest": "1.0.0",
|
||||||
|
"url": "https://github.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test3",
|
||||||
|
"slug": "test3",
|
||||||
|
"installed": True,
|
||||||
|
"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
|
||||||
|
# a new device
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_supervisor_info",
|
||||||
|
return_value=supervisor_mock_data,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.get_os_info",
|
||||||
|
return_value=os_mock_data,
|
||||||
|
):
|
||||||
|
async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(dev_reg.devices) == 3
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue