From 4f29e1e18015c27884d16a3ee709b951ba53a10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 9 Mar 2023 19:06:35 +0100 Subject: [PATCH] Add stats sensors for core and supervisor (#89455) * Add stats sensors for core and supervisor * Update homeassistant/components/hassio/__init__.py --- homeassistant/components/hassio/__init__.py | 36 ++++++++++++- homeassistant/components/hassio/handler.py | 16 ++++++ homeassistant/components/hassio/sensor.py | 52 +++++++++++++++++-- tests/components/hassio/test_binary_sensor.py | 32 ++++++++++++ tests/components/hassio/test_diagnostics.py | 32 ++++++++++++ tests/components/hassio/test_handler.py | 28 ++++++++++ tests/components/hassio/test_init.py | 48 ++++++++++++++--- tests/components/hassio/test_sensor.py | 32 ++++++++++++ tests/components/hassio/test_update.py | 32 ++++++++++++ 9 files changed, 295 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 23936f65767..25f3477bff1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -115,11 +115,13 @@ CONFIG_SCHEMA = vol.Schema( DATA_CORE_INFO = "hassio_core_info" +DATA_CORE_STATS = "hassio_core_stats" DATA_HOST_INFO = "hassio_host_info" DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" +DATA_SUPERVISOR_STATS = "hassio_supervisor_stats" DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" @@ -301,6 +303,26 @@ def get_addons_stats(hass): return hass.data.get(DATA_ADDONS_STATS) +@callback +@bind_hass +def get_core_stats(hass): + """Return core stats. + + Async friendly. + """ + return hass.data.get(DATA_CORE_STATS) + + +@callback +@bind_hass +def get_supervisor_stats(hass): + """Return supervisor stats. + + Async friendly. + """ + return hass.data.get(DATA_SUPERVISOR_STATS) + + @callback @bind_hass def get_addons_changelogs(hass): @@ -747,8 +769,14 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): if self.is_hass_os: new_data[DATA_KEY_OS] = get_os_info(self.hass) - new_data[DATA_KEY_CORE] = get_core_info(self.hass) - new_data[DATA_KEY_SUPERVISOR] = supervisor_info + new_data[DATA_KEY_CORE] = { + **(get_core_info(self.hass) or {}), + **get_core_stats(self.hass), + } + new_data[DATA_KEY_SUPERVISOR] = { + **supervisor_info, + **get_supervisor_stats(self.hass), + } # If this is the initial refresh, register all addons and return the dict if not self.data: @@ -805,12 +833,16 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): ( self.hass.data[DATA_INFO], self.hass.data[DATA_CORE_INFO], + self.hass.data[DATA_CORE_STATS], self.hass.data[DATA_SUPERVISOR_INFO], + self.hass.data[DATA_SUPERVISOR_STATS], self.hass.data[DATA_OS_INFO], ) = await asyncio.gather( self.hassio.get_info(), self.hassio.get_core_info(), + self.hassio.get_core_stats(), self.hassio.get_supervisor_info(), + self.hassio.get_supervisor_stats(), self.hassio.get_os_info(), ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 762df4f79ca..d7af26851d0 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -319,6 +319,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_core_stats(self): + """Return stats for the core. + + This method returns a coroutine. + """ + return self.send_command("/core/stats", method="get") + @api_data def get_addon_stats(self, addon): """Return stats for an Add-on. @@ -327,6 +335,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/stats", method="get") + @api_data + def get_supervisor_stats(self): + """Return stats for the supervisor. + + This method returns a coroutine. + """ + return self.send_command("/supervisor/stats", method="get") + def get_addon_changelog(self, addon): """Return changelog for an Add-on. diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 31e728a9736..a5b0b3a725f 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -18,9 +18,16 @@ from .const import ( ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, + DATA_KEY_CORE, DATA_KEY_OS, + DATA_KEY_SUPERVISOR, +) +from .entity import ( + HassioAddonEntity, + HassioCoreEntity, + HassioOSEntity, + HassioSupervisorEntity, ) -from .entity import HassioAddonEntity, HassioOSEntity COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( @@ -35,7 +42,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( ), ) -ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( +STATS_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_CPU_PERCENT, @@ -54,7 +61,10 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( ), ) +ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + STATS_ENTITY_DESCRIPTIONS +CORE_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS +SUPERVISOR_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS async def async_setup_entry( @@ -65,7 +75,9 @@ async def async_setup_entry( """Sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities: list[HassioOSSensor | HassioAddonSensor] = [] + entities: list[ + HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor + ] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): for entity_description in ADDON_ENTITY_DESCRIPTIONS: @@ -77,6 +89,22 @@ async def async_setup_entry( ) ) + for entity_description in CORE_ENTITY_DESCRIPTIONS: + entities.append( + CoreSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) + + for entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS: + entities.append( + SupervisorSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) + if coordinator.is_hass_os: for entity_description in OS_ENTITY_DESCRIPTIONS: entities.append( @@ -107,3 +135,21 @@ class HassioOSSensor(HassioOSEntity, SensorEntity): def native_value(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] + + +class CoreSensor(HassioCoreEntity, SensorEntity): + """Sensor to track a core attribute.""" + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_CORE][self.entity_description.key] + + +class SupervisorSensor(HassioSupervisorEntity, SensorEntity): + """Sensor to track a supervisor attribute.""" + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][self.entity_description.key] diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index 133074d7c9d..854a6782b1a 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -120,6 +120,38 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") aioclient_mock.get( "http://127.0.0.1/addons/test/info", diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 7b89cb6c99f..b3d47e93afd 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -125,6 +125,38 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") aioclient_mock.get( "http://127.0.0.1/addons/test/info", diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 64e9e1c31cc..c7075dba932 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -226,6 +226,34 @@ async def test_api_addon_stats( assert aioclient_mock.call_count == 1 +async def test_api_core_stats( + hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API Add-on stats.""" + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={"result": "ok", "data": {"memory_percent": 0.01}}, + ) + + data = await hassio_handler.get_core_stats() + assert data["memory_percent"] == 0.01 + assert aioclient_mock.call_count == 1 + + +async def test_api_supervisor_stats( + hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with API Add-on stats.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={"result": "ok", "data": {"memory_percent": 0.01}}, + ) + + data = await hassio_handler.get_supervisor_stats() + assert data["memory_percent"] == 0.01 + assert aioclient_mock.call_count == 1 + + async def test_api_discovery_message( hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index ee7e9cfa709..a752fe1b677 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -124,6 +124,38 @@ def mock_all(aioclient_mock, request, os_info): ], }, ) + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) aioclient_mock.get( "http://127.0.0.1/addons/test/stats", json={ @@ -210,7 +242,7 @@ async def test_setup_api_ping( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -254,7 +286,7 @@ async def test_setup_api_push_api_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 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"] @@ -273,7 +305,7 @@ async def test_setup_api_push_api_data_server_host( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 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"] @@ -290,7 +322,7 @@ async def test_setup_api_push_api_data_default( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 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"] @@ -370,7 +402,7 @@ async def test_setup_api_existing_hassio_user( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 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 @@ -387,7 +419,7 @@ async def test_setup_core_push_timezone( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -407,7 +439,7 @@ async def test_setup_hassio_no_additional_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -822,7 +854,7 @@ async def test_setup_hardware_integration( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 18 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 4088ba631f4..99b1db2a99b 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -113,6 +113,38 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") aioclient_mock.get( "http://127.0.0.1/addons/test/info", diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 20a46da0511..547e32dfddb 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -127,6 +127,38 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get( + "http://127.0.0.1/core/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") aioclient_mock.get( "http://127.0.0.1/addons/test/info",