From d23dbfb21474e728714e46f8e2bbb4a176ec5a88 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 21 Apr 2026 23:23:27 +0200 Subject: [PATCH] Add volumes to Portainer (#167326) --- .../components/portainer/coordinator.py | 54 +- homeassistant/components/portainer/entity.py | 55 ++ homeassistant/components/portainer/icons.json | 3 + homeassistant/components/portainer/sensor.py | 63 +- .../components/portainer/strings.json | 6 + tests/components/portainer/conftest.py | 5 + .../portainer/fixtures/docker_system_df.json | 25 +- .../portainer/fixtures/volumes.json | 62 ++ .../portainer/snapshots/test_init.ambr | 87 +++ .../portainer/snapshots/test_sensor.ambr | 638 ++++++++++++++++++ 10 files changed, 991 insertions(+), 7 deletions(-) create mode 100644 tests/components/portainer/fixtures/volumes.json diff --git a/homeassistant/components/portainer/coordinator.py b/homeassistant/components/portainer/coordinator.py index c83b4a87eeeb..cba2c6ef0a02 100644 --- a/homeassistant/components/portainer/coordinator.py +++ b/homeassistant/components/portainer/coordinator.py @@ -20,6 +20,8 @@ from pyportainer.models.docker import ( DockerContainer, DockerContainerStats, DockerSystemDF, + DockerVolume, + DockerVolumeUsageData, ) from pyportainer.models.docker_inspect import DockerInfo, DockerVersion from pyportainer.models.portainer import Endpoint @@ -52,6 +54,7 @@ class PortainerCoordinatorData: docker_info: DockerInfo docker_system_df: DockerSystemDF stacks: dict[str, PortainerStackData] + volumes: dict[str, PortainerVolumeData] @dataclass(slots=True) @@ -72,6 +75,13 @@ class PortainerStackData: container_count: int = 0 +@dataclass(slots=True) +class PortainerVolumeData: + """Volume data held by the Portainer coordinator.""" + + volume: DockerVolume + + class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorData]]): """Data Update Coordinator for Portainer.""" @@ -96,6 +106,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD self.known_endpoints: set[int] = set() self.known_containers: set[tuple[int, str]] = set() self.known_stacks: set[tuple[int, str]] = set() + self.known_volumes: set[tuple[int, str]] = set() self.new_endpoints_callbacks: list[ Callable[[list[PortainerCoordinatorData]], None] @@ -108,6 +119,9 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD self.new_stacks_callbacks: list[ Callable[[list[tuple[PortainerCoordinatorData, PortainerStackData]]], None] ] = [] + self.new_volumes_callbacks: list[ + Callable[[list[tuple[PortainerCoordinatorData, PortainerVolumeData]]], None] + ] = [] async def _async_setup(self) -> None: """Set up the Portainer Data Update Coordinator.""" @@ -170,11 +184,13 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD docker_version, docker_info, docker_system_df, + volumes, ) = await asyncio.gather( self.portainer.get_containers(endpoint.id), self.portainer.docker_version(endpoint.id), self.portainer.docker_info(endpoint.id), - self.portainer.docker_system_df(endpoint.id), + self.portainer.docker_system_df(endpoint.id, verbose=True), + self.portainer.get_volumes(endpoint.id), ) stack_requests = [self.portainer.get_stacks(endpoint_id=endpoint.id)] @@ -205,6 +221,19 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD for stack in stacks } + volume_usage_map = { + item["Name"]: item + for item in (docker_system_df.volume_disk_usage.items or []) + } + volume_map: dict[str, PortainerVolumeData] = {} + for volume in volumes: + if item := volume_usage_map.get(volume.name): + volume.usage_data = DockerVolumeUsageData( + size=item["UsageData"]["Size"], + ref_count=item["UsageData"]["RefCount"], + ) + volume_map[volume.name] = PortainerVolumeData(volume=volume) + # Map containers, started and stopped for container in containers: container_name = self._get_container_name(container.names[0]) @@ -286,6 +315,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD docker_version=docker_version, docker_info=docker_info, docker_system_df=docker_system_df, + volumes=volume_map, stacks=stack_map, ) @@ -332,6 +362,28 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD for container_callback in self.new_containers_callbacks: container_callback(new_container_data) + # Volume management + current_volumes = { + (endpoint.id, volume_name) + for endpoint in mapped_endpoints.values() + for volume_name in endpoint.volumes + } + + self.known_volumes &= current_volumes + new_volumes = current_volumes - self.known_volumes + if new_volumes: + _LOGGER.debug("New volumes found: %s", new_volumes) + self.known_volumes.update(new_volumes) + new_volume_data = [ + ( + mapped_endpoints[endpoint_id], + mapped_endpoints[endpoint_id].volumes[name], + ) + for endpoint_id, name in new_volumes + ] + for volume_callback in self.new_volumes_callbacks: + volume_callback(new_volume_data) + # Stack management current_stacks = { (endpoint.id, stack_name) diff --git a/homeassistant/components/portainer/entity.py b/homeassistant/components/portainer/entity.py index 9fb87248e633..04b61aaa3c4a 100644 --- a/homeassistant/components/portainer/entity.py +++ b/homeassistant/components/portainer/entity.py @@ -9,10 +9,12 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN from .coordinator import ( + DockerVolume, PortainerContainerData, PortainerCoordinator, PortainerCoordinatorData, PortainerStackData, + PortainerVolumeData, ) @@ -173,3 +175,56 @@ class PortainerStackEntity(PortainerCoordinatorEntity): def stack_data(self) -> PortainerStackData: """Return the coordinator data for this stack.""" return self.coordinator.data[self.endpoint_id].stacks[self.device_name] + + +class PortainerVolumeEntity(PortainerCoordinatorEntity): + """Base implementation for Portainer volume.""" + + def __init__( + self, + coordinator: PortainerCoordinator, + entity_description: EntityDescription, + device_info: DockerVolume, + via_device: PortainerCoordinatorData, + ) -> None: + """Initialize a Portainer volume.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._device_info = device_info + self.volume_name = device_info.name + self.endpoint_id = via_device.endpoint.id + self.endpoint_name = via_device.endpoint.name + + self._attr_device_info = DeviceInfo( + identifiers={ + ( + DOMAIN, + f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_volume_{self.volume_name}", + ) + }, + manufacturer=DEFAULT_NAME, + configuration_url=URL( + f"{coordinator.config_entry.data[CONF_URL]}#!/{self.endpoint_id}/docker/volumes/{self.volume_name}" + ), + model="Volume", + name=self.volume_name, + via_device=( + DOMAIN, + f"{coordinator.config_entry.entry_id}_{self.endpoint_id}", + ), + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_volume_{self.volume_name}_{entity_description.key}" + + @property + def available(self) -> bool: + """Return if the volume is available.""" + return ( + super().available + and self.endpoint_id in self.coordinator.data + and self.volume_name in self.coordinator.data[self.endpoint_id].volumes + ) + + @property + def volume_data(self) -> PortainerVolumeData: + """Return the coordinator data for this volume.""" + return self.coordinator.data[self.endpoint_id].volumes[self.volume_name] diff --git a/homeassistant/components/portainer/icons.json b/homeassistant/components/portainer/icons.json index 1f5c1a6a01ba..842cdc16fc23 100644 --- a/homeassistant/components/portainer/icons.json +++ b/homeassistant/components/portainer/icons.json @@ -95,6 +95,9 @@ }, "volume_disk_usage_total_size": { "default": "mdi:harddisk" + }, + "volume_driver": { + "default": "mdi:docker" } }, "switch": { diff --git a/homeassistant/components/portainer/sensor.py b/homeassistant/components/portainer/sensor.py index 8f5fdd2bdde1..c67418d25040 100644 --- a/homeassistant/components/portainer/sensor.py +++ b/homeassistant/components/portainer/sensor.py @@ -23,12 +23,14 @@ from .coordinator import ( PortainerConfigEntry, PortainerContainerData, PortainerStackData, + PortainerVolumeData, ) from .entity import ( PortainerContainerEntity, PortainerCoordinatorData, PortainerEndpointEntity, PortainerStackEntity, + PortainerVolumeEntity, ) PARALLEL_UPDATES = 0 @@ -55,6 +57,13 @@ class PortainerStackSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[PortainerStackData], StateType] +@dataclass(frozen=True, kw_only=True) +class PortainerVolumeSensorEntityDescription(SensorEntityDescription): + """Class to hold Portainer volume sensor description.""" + + value_fn: Callable[[PortainerVolumeData], StateType] + + CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = ( PortainerContainerSensorEntityDescription( key="image", @@ -287,7 +296,6 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), ) - STACK_SENSORS: tuple[PortainerStackSensorEntityDescription, ...] = ( PortainerStackSensorEntityDescription( key="stack_type", @@ -313,6 +321,25 @@ STACK_SENSORS: tuple[PortainerStackSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ), ) +VOLUME_SENSORS: tuple[PortainerVolumeSensorEntityDescription, ...] = ( + PortainerVolumeSensorEntityDescription( + key="volume_driver", + translation_key="volume_driver", + value_fn=lambda data: data.volume.driver, + ), + PortainerVolumeSensorEntityDescription( + key="volume_size", + translation_key="volume_size", + value_fn=lambda data: ( + data.volume.usage_data.size if data.volume.usage_data else None + ), + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) async def async_setup_entry( @@ -366,9 +393,25 @@ async def async_setup_entry( for entity_description in STACK_SENSORS ) + def _async_add_new_volumes( + volumes: list[tuple[PortainerCoordinatorData, PortainerVolumeData]], + ) -> None: + """Add new volume sensors.""" + async_add_entities( + PortainerVolumeSensor( + coordinator, + entity_description, + volume.volume, + endpoint, + ) + for (endpoint, volume) in volumes + for entity_description in VOLUME_SENSORS + ) + coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints) coordinator.new_containers_callbacks.append(_async_add_new_containers) coordinator.new_stacks_callbacks.append(_async_add_new_stacks) + coordinator.new_volumes_callbacks.append(_async_add_new_volumes) _async_add_new_endpoints( [ @@ -391,6 +434,13 @@ async def async_setup_entry( for stack in endpoint.stacks.values() ] ) + _async_add_new_volumes( + [ + (endpoint, volume) + for endpoint in coordinator.data.values() + for volume in endpoint.volumes.values() + ] + ) class PortainerContainerSensor(PortainerContainerEntity, SensorEntity): @@ -425,3 +475,14 @@ class PortainerStackSensor(PortainerStackEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" return self.entity_description.value_fn(self.stack_data) + + +class PortainerVolumeSensor(PortainerVolumeEntity, SensorEntity): + """Representation of a Portainer volume sensor.""" + + entity_description: PortainerVolumeSensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.volume_data) diff --git a/homeassistant/components/portainer/strings.json b/homeassistant/components/portainer/strings.json index bdd75a9f9234..c0fffcf504b1 100644 --- a/homeassistant/components/portainer/strings.json +++ b/homeassistant/components/portainer/strings.json @@ -177,6 +177,12 @@ }, "volume_disk_usage_total_size": { "name": "Volume disk usage total size" + }, + "volume_driver": { + "name": "Volume driver" + }, + "volume_size": { + "name": "Volume size" } }, "switch": { diff --git a/tests/components/portainer/conftest.py b/tests/components/portainer/conftest.py index 9dc4c53804ca..ac4634b55ea2 100644 --- a/tests/components/portainer/conftest.py +++ b/tests/components/portainer/conftest.py @@ -7,6 +7,7 @@ from pyportainer.models.docker import ( DockerContainer, DockerContainerStats, DockerSystemDF, + DockerVolume, ) from pyportainer.models.docker_inspect import DockerInfo, DockerVersion from pyportainer.models.portainer import Endpoint, PortainerSystemStatus @@ -81,6 +82,10 @@ def mock_portainer_client() -> Generator[AsyncMock]: client.portainer_system_status.return_value = PortainerSystemStatus.from_dict( load_json_value_fixture("portainer_system_status.json", DOMAIN) ) + client.get_volumes.return_value = [ + DockerVolume.from_dict(volume) + for volume in load_json_array_fixture("volumes.json", DOMAIN) + ] client.restart_container = AsyncMock(return_value=None) client.images_prune = AsyncMock(return_value=None) diff --git a/tests/components/portainer/fixtures/docker_system_df.json b/tests/components/portainer/fixtures/docker_system_df.json index d78d7a08852f..4c9d6463d586 100644 --- a/tests/components/portainer/fixtures/docker_system_df.json +++ b/tests/components/portainer/fixtures/docker_system_df.json @@ -1,26 +1,41 @@ { - "ImagesDiskUsage": { + "ImageUsage": { "ActiveCount": 1, "TotalCount": 4, "Reclaimable": 12345678, "TotalSize": 98765432, "Items": [] }, - "ContainersDiskUsage": { + "ContainerUsage": { "ActiveCount": 1, "TotalCount": 4, "Reclaimable": 12345678, "TotalSize": 98765432, "Items": [] }, - "VolumesDiskUsage": { + "VolumeUsage": { "ActiveCount": 1, "TotalCount": 4, "Reclaimable": 12345678, "TotalSize": 98765432, - "Items": [] + "Items": [ + { + "Name": "myvolume", + "UsageData": { + "Size": 104857600, + "RefCount": 2 + } + }, + { + "Name": "db_data", + "UsageData": { + "Size": 67108864, + "RefCount": 1 + } + } + ] }, - "BuildCacheDiskUsage": { + "BuildCacheUsage": { "ActiveCount": 1, "TotalCount": 4, "Reclaimable": 12345678, diff --git a/tests/components/portainer/fixtures/volumes.json b/tests/components/portainer/fixtures/volumes.json new file mode 100644 index 000000000000..92c5ef85b585 --- /dev/null +++ b/tests/components/portainer/fixtures/volumes.json @@ -0,0 +1,62 @@ +[ + { + "Name": "myvolume", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/myvolume/_data", + "CreatedAt": "2025-02-15T12:34:56.000000000Z", + "Status": { + "ref": "volume-local" + }, + "Labels": { + "com.example.some-label": "some-value", + "com.example.some-other-label": "some-other-value" + }, + "Scope": "local", + "Options": { + "type": "none", + "o": "bind", + "device": "/data/volumes/myvolume" + }, + "UsageData": { + "Size": 104857600, + "RefCount": 2 + } + }, + { + "Name": "db_data", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/db_data/_data", + "CreatedAt": "2025-02-10T08:11:22.000000000Z", + "Labels": { + "com.docker.compose.project": "webstack", + "com.docker.compose.volume": "db_data" + }, + "Scope": "local", + "Options": { + "type": "tmpfs", + "o": "size=64m,uid=1000", + "device": "tmpfs" + }, + "UsageData": { + "Size": 67108864, + "RefCount": 1 + } + }, + { + "Name": "dashy_config", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/dashy_config/_data", + "CreatedAt": "2025-02-18T19:45:10.000000000Z", + "Status": { + "ref": "volume-swarm" + }, + "Labels": { + "com.docker.stack.namespace": "dashy" + }, + "Scope": "local", + "UsageData": { + "Size": 3145728, + "RefCount": 1 + } + } +] diff --git a/tests/components/portainer/snapshots/test_init.ambr b/tests/components/portainer/snapshots/test_init.ambr index 75ff06d882f8..224586ddd10f 100644 --- a/tests/components/portainer/snapshots/test_init.ambr +++ b/tests/components/portainer/snapshots/test_init.ambr @@ -262,5 +262,92 @@ 'sw_version': None, 'via_device_id': , }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/volumes/myvolume', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'portainer', + 'portainer_test_entry_123_1_volume_myvolume', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Portainer', + 'model': 'Volume', + 'model_id': None, + 'name': 'myvolume', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/volumes/db_data', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'portainer', + 'portainer_test_entry_123_1_volume_db_data', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Portainer', + 'model': 'Volume', + 'model_id': None, + 'name': 'db_data', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/volumes/dashy_config', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'portainer', + 'portainer_test_entry_123_1_volume_dashy_config', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Portainer', + 'model': 'Volume', + 'model_id': None, + 'name': 'dashy_config', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': , + }), ]) # --- diff --git a/tests/components/portainer/snapshots/test_sensor.ambr b/tests/components/portainer/snapshots/test_sensor.ambr index b3710a029f47..d3070c0d237e 100644 --- a/tests/components/portainer/snapshots/test_sensor.ambr +++ b/tests/components/portainer/snapshots/test_sensor.ambr @@ -1,4 +1,115 @@ # serializer version: 1 +# name: test_all_entities[sensor.dashy_config_volume_driver-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dashy_config_volume_driver', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume driver', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume driver', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_driver', + 'unique_id': 'portainer_test_entry_123_1_volume_dashy_config_volume_driver', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.dashy_config_volume_driver-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'dashy_config Volume driver', + }), + 'context': , + 'entity_id': 'sensor.dashy_config_volume_driver', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'local', + }) +# --- +# name: test_all_entities[sensor.dashy_config_volume_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dashy_config_volume_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_size', + 'unique_id': 'portainer_test_entry_123_1_volume_dashy_config_volume_size', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.dashy_config_volume_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'dashy_config Volume size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dashy_config_volume_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0029296875', + }) +# --- # name: test_all_entities[sensor.dashy_containers-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -469,6 +580,117 @@ 'state': 'swarm', }) # --- +# name: test_all_entities[sensor.db_data_volume_driver-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.db_data_volume_driver', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume driver', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume driver', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_driver', + 'unique_id': 'portainer_test_entry_123_1_volume_db_data_volume_driver', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.db_data_volume_driver-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'db_data Volume driver', + }), + 'context': , + 'entity_id': 'sensor.db_data_volume_driver', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'local', + }) +# --- +# name: test_all_entities[sensor.db_data_volume_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.db_data_volume_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_size', + 'unique_id': 'portainer_test_entry_123_1_volume_db_data_volume_size', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.db_data_volume_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'db_data Volume size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.db_data_volume_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0625', + }) +# --- # name: test_all_entities[sensor.focused_einstein_cpu_usage_total-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1330,6 +1552,128 @@ 'state': '14', }) # --- +# name: test_all_entities[sensor.my_environment_container_disk_usage_reclaimable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_environment_container_disk_usage_reclaimable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Container disk usage reclaimable', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container disk usage reclaimable', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_disk_usage_reclaimable', + 'unique_id': 'portainer_test_entry_123_1_container_disk_usage_reclaimable', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_environment_container_disk_usage_reclaimable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'my-environment Container disk usage reclaimable', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_environment_container_disk_usage_reclaimable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.7737560272217', + }) +# --- +# name: test_all_entities[sensor.my_environment_container_disk_usage_total_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_environment_container_disk_usage_total_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Container disk usage total size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container disk usage total size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_disk_usage_total_size', + 'unique_id': 'portainer_test_entry_123_1_container_disk_usage_total_size', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_environment_container_disk_usage_total_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'my-environment Container disk usage total size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_environment_container_disk_usage_total_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '94.190055847168', + }) +# --- # name: test_all_entities[sensor.my_environment_containers_paused-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1592,6 +1936,128 @@ 'state': '508', }) # --- +# name: test_all_entities[sensor.my_environment_image_disk_usage_reclaimable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_environment_image_disk_usage_reclaimable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Image disk usage reclaimable', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Image disk usage reclaimable', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'image_disk_usage_reclaimable', + 'unique_id': 'portainer_test_entry_123_1_image_disk_usage_reclaimable', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_environment_image_disk_usage_reclaimable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'my-environment Image disk usage reclaimable', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_environment_image_disk_usage_reclaimable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.7737560272217', + }) +# --- +# name: test_all_entities[sensor.my_environment_image_disk_usage_total_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_environment_image_disk_usage_total_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Image disk usage total size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Image disk usage total size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'image_disk_usage_total_size', + 'unique_id': 'portainer_test_entry_123_1_image_disk_usage_total_size', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_environment_image_disk_usage_total_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'my-environment Image disk usage total size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_environment_image_disk_usage_total_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '94.190055847168', + }) +# --- # name: test_all_entities[sensor.my_environment_kernel_version-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1856,6 +2322,178 @@ 'state': '1998.7890625', }) # --- +# name: test_all_entities[sensor.my_environment_volume_disk_usage_total_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_environment_volume_disk_usage_total_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume disk usage total size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume disk usage total size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_disk_usage_total_size', + 'unique_id': 'portainer_test_entry_123_1_volume_disk_usage_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.my_environment_volume_disk_usage_total_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'my-environment Volume disk usage total size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_environment_volume_disk_usage_total_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '94.190055847168', + }) +# --- +# name: test_all_entities[sensor.myvolume_volume_driver-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myvolume_volume_driver', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume driver', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume driver', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_driver', + 'unique_id': 'portainer_test_entry_123_1_volume_myvolume_volume_driver', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.myvolume_volume_driver-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'myvolume Volume driver', + }), + 'context': , + 'entity_id': 'sensor.myvolume_volume_driver', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'local', + }) +# --- +# name: test_all_entities[sensor.myvolume_volume_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.myvolume_volume_size', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Volume size', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume size', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume_size', + 'unique_id': 'portainer_test_entry_123_1_volume_myvolume_volume_size', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.myvolume_volume_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'myvolume Volume size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myvolume_volume_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.09765625', + }) +# --- # name: test_all_entities[sensor.practical_morse_cpu_usage_total-entry] EntityRegistryEntrySnapshot({ 'aliases': list([