mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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
@ -377,6 +377,9 @@ omit =
|
||||
homeassistant/components/harmony/data.py
|
||||
homeassistant/components/harmony/remote.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/hdmi_cec/*
|
||||
homeassistant/components/heatmiser/climate.py
|
||||
|
@ -1,24 +1,29 @@
|
||||
"""Support for Hass.io."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
ATTR_SERVICE,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
SERVICE_HOMEASSISTANT_RESTART,
|
||||
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
|
||||
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.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@ -32,7 +37,11 @@ from .const import (
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_INPUT,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SLUG,
|
||||
ATTR_SNAPSHOT,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .discovery import async_setup_discovery_view
|
||||
@ -46,6 +55,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
PLATFORMS = ["binary_sensor", "sensor"]
|
||||
|
||||
CONF_FRONTEND_REPO = "development_repo"
|
||||
|
||||
@ -62,9 +72,12 @@ DATA_OS_INFO = "hassio_os_info"
|
||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
|
||||
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
|
||||
SERVICE_ADDON_START = "addon_start"
|
||||
SERVICE_ADDON_STOP = "addon_stop"
|
||||
SERVICE_ADDON_RESTART = "addon_restart"
|
||||
SERVICE_ADDON_UPDATE = "addon_update"
|
||||
SERVICE_ADDON_STDIN = "addon_stdin"
|
||||
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
||||
SERVICE_HOST_REBOOT = "host_reboot"
|
||||
@ -110,6 +123,7 @@ MAP_SERVICE_API = {
|
||||
SERVICE_ADDON_START: ("/addons/{addon}/start", 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_UPDATE: ("/addons/{addon}/update", SCHEMA_ADDON, 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_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False),
|
||||
@ -286,13 +300,17 @@ def get_supervisor_ip():
|
||||
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."""
|
||||
# Check local setup
|
||||
for env in ("HASSIO", "HASSIO_TOKEN"):
|
||||
if os.environ.get(env):
|
||||
continue
|
||||
_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
|
||||
|
||||
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_SUPERVISOR_INFO] = await hassio.get_supervisor_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:
|
||||
_LOGGER.warning("Can't read last version: %s", err)
|
||||
|
||||
@ -455,4 +475,143 @@ async def async_setup(hass, config):
|
||||
# Init add-on ingress panels
|
||||
await async_setup_addon_panel(hass, hassio)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||
)
|
||||
|
||||
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_IS_ADMIN = "X-Hass-Is-Admin"
|
||||
|
||||
|
||||
WS_TYPE = "type"
|
||||
WS_ID = "id"
|
||||
|
||||
@ -38,3 +37,11 @@ WS_TYPE_EVENT = "supervisor/event"
|
||||
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
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:
|
||||
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:
|
||||
name: 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."""
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
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.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"}
|
||||
|
||||
|
||||
@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):
|
||||
"""Test setup with API ping."""
|
||||
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_stop")
|
||||
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", "host_shutdown")
|
||||
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/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/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/host/shutdown", 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_stop", {"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(
|
||||
"hassio", "addon_stdin", {"addon": "test", "input": "test"}
|
||||
)
|
||||
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"
|
||||
|
||||
await hass.services.async_call("hassio", "host_shutdown", {})
|
||||
await hass.services.async_call("hassio", "host_reboot", {})
|
||||
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(
|
||||
@ -247,7 +324,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||
)
|
||||
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] == {
|
||||
"addons": ["test"],
|
||||
"folders": ["ssl"],
|
||||
@ -268,7 +345,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||
)
|
||||
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] == {
|
||||
"addons": ["test"],
|
||||
"folders": ["ssl"],
|
||||
@ -302,3 +379,143 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
||||
assert mock_check_config.called
|
||||
|
||||
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…
x
Reference in New Issue
Block a user