diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0f17c0b5272..25482ddde95 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -69,6 +69,7 @@ from .const import ( ATTR_VERSION, DATA_KEY_ADDONS, DATA_KEY_CORE, + DATA_KEY_HOST, DATA_KEY_OS, DATA_KEY_SUPERVISOR, DOMAIN, @@ -668,6 +669,22 @@ def async_register_os_in_dev_reg( dev_reg.async_get_or_create(config_entry_id=entry_id, **params) +@callback +def async_register_host_in_dev_reg( + entry_id: str, + dev_reg: dr.DeviceRegistry, +) -> None: + """Register host in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "host")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.HOST, + name="Home Assistant Host", + entry_type=dr.DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + @callback def async_register_core_in_dev_reg( entry_id: str, @@ -777,6 +794,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): **supervisor_info, **get_supervisor_stats(self.hass), } + new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {} # If this is the initial refresh, register all addons and return the dict if not self.data: @@ -789,6 +807,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async_register_supervisor_in_dev_reg( self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR] ) + async_register_host_in_dev_reg(self.entry_id, self.dev_reg) if self.is_hass_os: async_register_os_in_dev_reg( self.entry_id, self.dev_reg, new_data[DATA_KEY_OS] diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 2710e146540..cc9c58a3d27 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -68,6 +68,7 @@ DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" DATA_KEY_SUPERVISOR = "supervisor" DATA_KEY_CORE = "core" +DATA_KEY_HOST = "host" class SupervisorEntityModel(str, Enum): @@ -77,3 +78,4 @@ class SupervisorEntityModel(str, Enum): OS = "Home Assistant Operating System" CORE = "Home Assistant Core" SUPERVIOSR = "Home Assistant Supervisor" + HOST = "Home Assistant Host" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index dfa89ae911a..3a6a5a9f7c3 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -11,6 +11,7 @@ from .const import ( ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_CORE, + DATA_KEY_HOST, DATA_KEY_OS, DATA_KEY_SUPERVISOR, ) @@ -71,6 +72,32 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): ) +class HassioHostEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): + """Base Entity for Hass.io host.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = f"home_assistant_host_{entity_description.key}" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "host")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and DATA_KEY_HOST in self.coordinator.data + and self.entity_description.key in self.coordinator.data[DATA_KEY_HOST] + ) + + class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Supervisor.""" diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index a5b0b3a725f..b9a97adcbc2 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -2,12 +2,13 @@ from __future__ import annotations from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,12 +20,14 @@ from .const import ( ATTR_VERSION_LATEST, DATA_KEY_ADDONS, DATA_KEY_CORE, + DATA_KEY_HOST, DATA_KEY_OS, DATA_KEY_SUPERVISOR, ) from .entity import ( HassioAddonEntity, HassioCoreEntity, + HassioHostEntity, HassioOSEntity, HassioSupervisorEntity, ) @@ -66,6 +69,45 @@ CORE_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS SUPERVISOR_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS +HOST_ENTITY_DESCRIPTIONS = ( + SensorEntityDescription( + entity_registry_enabled_default=False, + key="agent_version", + name="OS Agent version", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key="apparmor_version", + name="Apparmor version", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key="disk_total", + name="Disk total", + native_unit_of_measurement=UnitOfInformation.GIGABYTES, + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key="disk_used", + name="Disk used", + native_unit_of_measurement=UnitOfInformation.GIGABYTES, + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key="disk_free", + name="Disk free", + native_unit_of_measurement=UnitOfInformation.GIGABYTES, + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -76,7 +118,7 @@ async def async_setup_entry( coordinator = hass.data[ADDONS_COORDINATOR] entities: list[ - HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor + HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor ] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): @@ -105,6 +147,14 @@ async def async_setup_entry( ) ) + for entity_description in HOST_ENTITY_DESCRIPTIONS: + entities.append( + HostSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) + if coordinator.is_hass_os: for entity_description in OS_ENTITY_DESCRIPTIONS: entities.append( @@ -153,3 +203,12 @@ class SupervisorSensor(HassioSupervisorEntity, SensorEntity): def native_value(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_SUPERVISOR][self.entity_description.key] + + +class HostSensor(HassioHostEntity, SensorEntity): + """Sensor to track a host attribute.""" + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_HOST][self.entity_description.key] diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index b3d47e93afd..6b0dae170c6 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -211,5 +211,6 @@ async def test_diagnostics( assert "core" in diagnostics["coordinator_data"] assert "supervisor" in diagnostics["coordinator_data"] assert "os" in diagnostics["coordinator_data"] + assert "host" in diagnostics["coordinator_data"] - assert len(diagnostics["devices"]) == 5 + assert len(diagnostics["devices"]) == 6 diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index a752fe1b677..ead65d81292 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -678,7 +678,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: 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) == 5 + assert len(dev_reg.devices) == 6 supervisor_mock_data = { "version": "1.0.0", @@ -709,11 +709,11 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 4 + assert len(dev_reg.devices) == 5 async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 4 + assert len(dev_reg.devices) == 5 supervisor_mock_data = { "version": "1.0.0", @@ -763,7 +763,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 4 + assert len(dev_reg.devices) == 5 async def test_coordinator_updates( diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 99b1db2a99b..d33c6697321 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -44,12 +44,10 @@ def mock_all(aioclient_mock, request): json={ "result": "ok", "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, + "agent_version": "1.0.0", + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", }, }, ) @@ -179,6 +177,9 @@ def mock_all(aioclient_mock, request): [ ("sensor.home_assistant_operating_system_version", "1.0.0"), ("sensor.home_assistant_operating_system_newest_version", "1.0.0"), + ("sensor.home_assistant_host_os_agent_version", "1.0.0"), + ("sensor.home_assistant_core_cpu_percent", "0.99"), + ("sensor.home_assistant_supervisor_cpu_percent", "0.99"), ("sensor.test_version", "2.0.0"), ("sensor.test_newest_version", "2.0.1"), ("sensor.test2_version", "3.1.0"),