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:
Raman Gupta 2021-03-01 03:41:04 -05:00 committed by GitHub
parent d2db58d138
commit 0592309b65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 661 additions and 10 deletions

View file

@ -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

View file

@ -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

View 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]

View 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={})

View file

@ -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"

View 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")}}

View 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]

View file

@ -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.

View 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"

View file

@ -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