diff --git a/homeassistant/components/shelly/icons.json b/homeassistant/components/shelly/icons.json index 573c5e16fec5..9411a0b6745e 100644 --- a/homeassistant/components/shelly/icons.json +++ b/homeassistant/components/shelly/icons.json @@ -59,6 +59,9 @@ "self_test": { "default": "mdi:progress-wrench" }, + "storage_free_space": { + "default": "mdi:sd" + }, "tilt": { "default": "mdi:angle-acute" }, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index bc0e7d1fec19..85ad8cd92658 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -26,6 +26,7 @@ from homeassistant.const import ( UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, + UnitOfInformation, UnitOfPower, UnitOfPressure, UnitOfRatio, @@ -1732,6 +1733,18 @@ RPC_SENSORS: Final = { and right.get("vial", {}).get("level", -1) != -1 ), ), + "storage_fs_free": RpcSensorDescription( + key="storage", + sub_key="fs_free", + translation_key="storage_free_space", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + removal_condition=lambda _, status, key: not status[key]["present"], + ), } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 05cf32afe55d..17f4be9a709b 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -527,6 +527,9 @@ "session_energy": { "name": "Session energy" }, + "storage_free_space": { + "name": "Free storage space" + }, "temperature_with_channel_name": { "name": "{channel_name} temperature" }, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index afe922ed3735..e3fa5aa752b7 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -3,7 +3,7 @@ from copy import deepcopy from unittest.mock import Mock, PropertyMock -from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_EM3 +from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_CAMERA, MODEL_EM3 from aioshelly.exceptions import NotInitialized from freezegun.api import FrozenDateTimeFactory import pytest @@ -34,6 +34,7 @@ from homeassistant.const import ( UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, + UnitOfInformation, UnitOfPower, UnitOfTemperature, UnitOfVolume, @@ -2326,3 +2327,69 @@ async def test_rpc_sensor_errors_none( assert (state := hass.states.get("sensor.test_name_temperature")) assert state.state == "11.1" + + +async def test_rpc_storage_fs_free_sensor( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC storage free space sensor.""" + status = {"storage:0": {"present": True, "fs_free": 1048576000}} + monkeypatch.setattr(mock_rpc_device, "status", status) + + config = {"storage:0": {"id": 0}} + monkeypatch.setattr(mock_rpc_device, "config", config) + + entity_id = f"{SENSOR_DOMAIN}.test_name_free_storage_space" + await init_integration(hass, 4, model=MODEL_CAMERA) + + assert (state := hass.states.get(entity_id)) + assert state.state == "1.048576" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.GIGABYTES + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DATA_SIZE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + assert (entry := entity_registry.async_get(entity_id)) + assert entry.unique_id == "123456789ABC-storage:0-storage_fs_free" + assert entry.unit_of_measurement == UnitOfInformation.GIGABYTES + + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "storage:0", "fs_free", 2097152000 + ) + mock_rpc_device.mock_update() + + assert (state := hass.states.get(entity_id)) + assert state.state == "2.097152" + + +async def test_rpc_storage_fs_free_sensor_removal( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, +) -> None: + """Test RPC storage free space sensor removal when storage not present.""" + status = {"storage:0": {"present": False, "fs_free": 0}} + monkeypatch.setattr(mock_rpc_device, "status", status) + + config = {"storage:0": {"id": 0}} + monkeypatch.setattr(mock_rpc_device, "config", config) + + config_entry = await init_integration(hass, 4, model=MODEL_CAMERA, skip_setup=True) + device_entry = register_device(device_registry, config_entry) + entity_id = register_entity( + hass, + SENSOR_DOMAIN, + "test_name_free_storage_space", + "storage:0-storage_fs_free", + config_entry, + device_id=device_entry.id, + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get(entity_id) is None