diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1180e0a01d2..6484c85b95e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -42,9 +42,11 @@ CONFIG_SCHEMA = vol.Schema( ) -DATA_INFO = "hassio_info" -DATA_HOST_INFO = "hassio_host_info" DATA_CORE_INFO = "hassio_core_info" +DATA_HOST_INFO = "hassio_host_info" +DATA_INFO = "hassio_info" +DATA_OS_INFO = "hassio_os_info" +DATA_SUPERVISOR_INFO = "hassio_supervisor_info" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) SERVICE_ADDON_START = "addon_start" @@ -218,6 +220,26 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_supervisor_info(hass): + """Return Supervisor information. + + Async friendly. + """ + return hass.data.get(DATA_SUPERVISOR_INFO) + + +@callback +@bind_hass +def get_os_info(hass): + """Return OS information. + + Async friendly. + """ + return hass.data.get(DATA_OS_INFO) + + @callback @bind_hass def get_core_info(hass): @@ -358,6 +380,8 @@ async def async_setup(hass, config): hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() 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() except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 58dd9db6623..6bc3cb345a5 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -82,6 +82,14 @@ class HassIO: """ return self.send_command("/host/info", method="get") + @api_data + def get_os_info(self): + """Return data for the OS. + + This method return a coroutine. + """ + return self.send_command("/os/info", method="get") + @api_data def get_core_info(self): """Return data for Home Asssistant Core. @@ -90,6 +98,14 @@ class HassIO: """ return self.send_command("/core/info", method="get") + @api_data + def get_supervisor_info(self): + """Return data for the Supervisor. + + This method returns a coroutine. + """ + return self.send_command("/supervisor/info", method="get") + @api_data def get_addon_info(self, addon): """Return data for a Add-on. diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 193c9640cd5..ba969a4af3a 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -1,6 +1,6 @@ { "domain": "hassio", - "name": "Hass.io", + "name": "Home Assistant Supervisor", "documentation": "https://www.home-assistant.io/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json new file mode 100644 index 00000000000..875a79a60d7 --- /dev/null +++ b/homeassistant/components/hassio/strings.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "board": "Board", + "disk_total": "Disk Total", + "disk_used": "Disk Used", + "docker_version": "Docker Version", + "healthy": "Healthy", + "host_os": "Host Operating System", + "installed_addons": "Installed Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Version", + "supported": "Supported", + "update_channel": "Update Channel", + "version_api": "Version API" + } + } +} diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py new file mode 100644 index 00000000000..530703d3e25 --- /dev/null +++ b/homeassistant/components/hassio/system_health.py @@ -0,0 +1,72 @@ +"""Provide info to system health.""" +import os + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +SUPERVISOR_PING = f"http://{os.environ['HASSIO']}/supervisor/ping" +OBSERVER_URL = f"http://{os.environ['HASSIO']}:4357" + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info, "/hassio") + + +async def system_health_info(hass: HomeAssistant): + """Get info for the info page.""" + info = hass.components.hassio.get_info() + host_info = hass.components.hassio.get_host_info() + supervisor_info = hass.components.hassio.get_supervisor_info() + + if supervisor_info.get("healthy"): + healthy = True + else: + healthy = { + "type": "failed", + "error": "Unhealthy", + "more_info": "/hassio/system", + } + + if supervisor_info.get("supported"): + supported = True + else: + supported = { + "type": "failed", + "error": "Unsupported", + "more_info": "/hassio/system", + } + + information = { + "host_os": host_info.get("operating_system"), + "update_channel": info.get("channel"), + "supervisor_version": info.get("supervisor"), + "docker_version": info.get("docker"), + "disk_total": f"{host_info.get('disk_total')} GB", + "disk_used": f"{host_info.get('disk_used')} GB", + "healthy": healthy, + "supported": supported, + } + + if info.get("hassos") is not None: + os_info = hass.components.hassio.get_os_info() + information["board"] = os_info.get("board") + + information["supervisor_api"] = system_health.async_check_can_reach_url( + hass, SUPERVISOR_PING, OBSERVER_URL + ) + information["version_api"] = system_health.async_check_can_reach_url( + hass, + f"https://version.home-assistant.io/{info.get('channel')}.json", + "/hassio/system", + ) + + information["installed_addons"] = ", ".join( + f"{addon['name']} ({addon['version']})" + for addon in supervisor_info.get("addons", []) + ) + + return information diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 981cb51c83a..875a79a60d7 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,3 +1,18 @@ { - "title": "Hass.io" -} \ No newline at end of file + "system_health": { + "info": { + "board": "Board", + "disk_total": "Disk Total", + "disk_used": "Disk Used", + "docker_version": "Docker Version", + "healthy": "Healthy", + "host_os": "Host Operating System", + "installed_addons": "Installed Add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor Version", + "supported": "Supported", + "update_channel": "Update Channel", + "version_api": "Version API" + } + } +} diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e349cc1cb83..1aa414c4984 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -1,20 +1,16 @@ { "system_health": { "info": { - "installation_type": "Installation Type", - "version": "Version", - "dev": "Development", - "virtualenv": "Virtual Environment", - "python_version": "Python Version", - "docker": "Docker", "arch": "CPU Architecture", - "timezone": "Timezone", - "os_name": "Operating System Name", + "dev": "Development", + "docker": "Docker", + "installation_type": "Installation Type", + "os_name": "Operating System Family", "os_version": "Operating System Version", - "supervisor": "Supervisor", - "host_os": "Home Assistant OS", - "chassis": "Chassis", - "docker_version": "Docker" + "python_version": "Python Version", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" } } } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index af1be3bd0a5..b0245d9beec 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -15,5 +15,17 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" info = await system_info.async_get_system_info(hass) - info.pop("hassio") - return info + + return { + "version": info.get("version"), + "installation_type": info.get("installation_type"), + "dev": info.get("dev"), + "hassio": info.get("hassio"), + "docker": info.get("docker"), + "virtualenv": info.get("virtualenv"), + "python_version": info.get("python_version"), + "os_name": info.get("os_name"), + "os_version": info.get("os_version"), + "arch": info.get("arch"), + "timezone": info.get("timezone"), + } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 9885785fa4d..8e810ef2143 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,20 +1,17 @@ { - "system_health": { - "info": { - "arch": "CPU Architecture", - "chassis": "Chassis", - "dev": "Development", - "docker": "Docker", - "docker_version": "Docker", - "host_os": "Home Assistant OS", - "installation_type": "Installation Type", - "os_name": "Operating System Name", - "os_version": "Operating System Version", - "python_version": "Python Version", - "supervisor": "Supervisor", - "timezone": "Timezone", - "version": "Version", - "virtualenv": "Virtual Environment" - } + "system_health": { + "info": { + "arch": "CPU Architecture", + "dev": "Development", + "docker": "Docker", + "installation_type": "Installation Type", + "os_name": "Operating System Family", + "os_version": "Operating System Version", + "python_version": "Python Version", + "timezone": "Timezone", + "version": "Version", + "virtualenv": "Virtual Environment" } -} \ No newline at end of file + }, + "title": "Home Assistant" +} diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 311fc6c7e8c..33fb00b4485 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -80,6 +80,39 @@ async def test_api_host_info(hassio_handler, aioclient_mock): assert data["operating_system"] == "Debian GNU/Linux 10 (buster)" +async def test_api_supervisor_info(hassio_handler, aioclient_mock): + """Test setup with API Supervisor info.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": {"supported": True, "version": "2020.11.1", "channel": "stable"}, + }, + ) + + data = await hassio_handler.get_supervisor_info() + assert aioclient_mock.call_count == 1 + assert data["supported"] + assert data["version"] == "2020.11.1" + assert data["channel"] == "stable" + + +async def test_api_os_info(hassio_handler, aioclient_mock): + """Test setup with API OS info.""" + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": {"board": "odroid-n2", "version": "2020.11.1"}, + }, + ) + + data = await hassio_handler.get_os_info() + assert aioclient_mock.call_count == 1 + assert data["board"] == "odroid-n2" + assert data["version"] == "2020.11.1" + + async def test_api_host_info_error(hassio_handler, aioclient_mock): """Test setup with API Home Assistant info error.""" aioclient_mock.get( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 62b4a4adbd2..214551bc3b7 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -44,6 +44,14 @@ def mock_all(aioclient_mock): "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"}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @@ -55,7 +63,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -94,7 +102,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -110,7 +118,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -122,7 +130,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -169,7 +177,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -183,7 +191,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -200,7 +208,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py new file mode 100644 index 00000000000..cd6cb2d939f --- /dev/null +++ b/tests/components/hassio/test_system_health.py @@ -0,0 +1,113 @@ +"""Test hassio system health.""" +import asyncio +import os + +from aiohttp import ClientError + +from homeassistant.setup import async_setup_component + +from .test_init import MOCK_ENVIRON + +from tests.async_mock import patch +from tests.common import get_system_health_info + + +async def test_hassio_system_health(hass, aioclient_mock): + """Test hassio system health.""" + aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") + aioclient_mock.get("https://version.home-assistant.io/stable.json", text="") + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} + ) + + hass.config.components.add("hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, "system_health", {}) + + hass.data["hassio_info"] = { + "channel": "stable", + "supervisor": "2020.11.1", + "docker": "19.0.3", + "hassos": True, + } + hass.data["hassio_host_info"] = { + "operating_system": "Home Assistant OS 5.9", + "disk_total": "32.0", + "disk_used": "30.0", + } + hass.data["hassio_os_info"] = {"board": "odroid-n2"} + hass.data["hassio_supervisor_info"] = { + "healthy": True, + "supported": True, + "addons": [{"name": "Awesome Addon", "version": "1.0.0"}], + } + + info = await get_system_health_info(hass, "hassio") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == { + "board": "odroid-n2", + "disk_total": "32.0 GB", + "disk_used": "30.0 GB", + "docker_version": "19.0.3", + "healthy": True, + "host_os": "Home Assistant OS 5.9", + "installed_addons": "Awesome Addon (1.0.0)", + "supervisor_api": "ok", + "supervisor_version": "2020.11.1", + "supported": True, + "update_channel": "stable", + "version_api": "ok", + } + + +async def test_hassio_system_health_with_issues(hass, aioclient_mock): + """Test hassio system health.""" + aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") + aioclient_mock.get("https://version.home-assistant.io/stable.json", exc=ClientError) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} + ) + + hass.config.components.add("hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component(hass, "system_health", {}) + + hass.data["hassio_info"] = {"channel": "stable"} + hass.data["hassio_host_info"] = {} + hass.data["hassio_os_info"] = {} + hass.data["hassio_supervisor_info"] = { + "healthy": False, + "supported": False, + } + + info = await get_system_health_info(hass, "hassio") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["healthy"] == { + "error": "Unhealthy", + "more_info": "/hassio/system", + "type": "failed", + } + assert info["supported"] == { + "error": "Unsupported", + "more_info": "/hassio/system", + "type": "failed", + } + assert info["version_api"] == { + "error": "unreachable", + "more_info": "/hassio/system", + "type": "failed", + } diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index a4826c00328..73845aba7b2 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -72,6 +72,12 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": {}},