From 25970a4f600d03be4496dcaa9afb513671847631 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 29 Sep 2026 18:27:13 +0200 Subject: [PATCH] Add out of memory binary sensor to Portainer (#183224) --- .../components/portainer/binary_sensor.py | 14 +- .../components/portainer/strings.json | 3 + .../snapshots/test_binary_sensor.ambr | 306 ++++++++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/portainer/binary_sensor.py b/homeassistant/components/portainer/binary_sensor.py index c39f8727f8f8..033764f1d133 100644 --- a/homeassistant/components/portainer/binary_sensor.py +++ b/homeassistant/components/portainer/binary_sensor.py @@ -33,6 +33,7 @@ class PortainerContainerBinarySensorEntityDescription(BinarySensorEntityDescript """Class to hold Portainer container binary sensor description.""" state_fn: Callable[[PortainerContainerData], bool | None] + supported_fn: Callable[[PortainerContainerData], bool] @dataclass(frozen=True, kw_only=True) @@ -54,9 +55,20 @@ CONTAINER_SENSORS: tuple[PortainerContainerBinarySensorEntityDescription, ...] = key="status", translation_key="status", state_fn=lambda data: data.container.state == DockerContainerState.RUNNING, + supported_fn=lambda data: data.container.state == DockerContainerState.RUNNING, device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, ), + PortainerContainerBinarySensorEntityDescription( + key="container_out_of_memory", + translation_key="container_out_of_memory", + state_fn=lambda data: ( + state.oom_killed if (state := data.container_inspect.state) else None + ), + supported_fn=lambda data: data.container_inspect.state is not None, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) ENDPOINT_SENSORS: tuple[PortainerEndpointBinarySensorEntityDescription, ...] = ( @@ -114,7 +126,7 @@ async def async_setup_entry( ) for (endpoint, container) in containers for entity_description in CONTAINER_SENSORS - if entity_description.state_fn(container) + if entity_description.supported_fn(container) ) def _async_add_new_stacks( diff --git a/homeassistant/components/portainer/strings.json b/homeassistant/components/portainer/strings.json index 505378b0dff0..8197aab904c4 100644 --- a/homeassistant/components/portainer/strings.json +++ b/homeassistant/components/portainer/strings.json @@ -56,6 +56,9 @@ }, "entity": { "binary_sensor": { + "container_out_of_memory": { + "name": "Out of memory" + }, "status": { "name": "Status" } diff --git a/tests/components/portainer/snapshots/test_binary_sensor.ambr b/tests/components/portainer/snapshots/test_binary_sensor.ambr index c89f305eb8f4..dbc927c2e68d 100644 --- a/tests/components/portainer/snapshots/test_binary_sensor.ambr +++ b/tests/components/portainer/snapshots/test_binary_sensor.ambr @@ -1,4 +1,55 @@ # serializer version: 1 +# name: test_all_entities[binary_sensor.dashy_dashy_1_qgza68hnz4n1qvyz3iohynx05_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dashy_dashy_1_qgza68hnz4n1qvyz3iohynx05_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_dashy_dashy.1.qgza68hnz4n1qvyz3iohynx05_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.dashy_dashy_1_qgza68hnz4n1qvyz3iohynx05_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'dashy_dashy.1.qgza68hnz4n1qvyz3iohynx05 Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.dashy_dashy_1_qgza68hnz4n1qvyz3iohynx05_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.dashy_dashy_1_qgza68hnz4n1qvyz3iohynx05_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -101,6 +152,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[binary_sensor.focused_einstein_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.focused_einstein_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_focused_einstein_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.focused_einstein_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'focused_einstein Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.focused_einstein_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.focused_einstein_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -152,6 +254,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[binary_sensor.funny_chatelet_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.funny_chatelet_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_funny_chatelet_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.funny_chatelet_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'funny_chatelet Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.funny_chatelet_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.funny_chatelet_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -254,6 +407,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[binary_sensor.practical_morse_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.practical_morse_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_practical_morse_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.practical_morse_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'practical_morse Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.practical_morse_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.practical_morse_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -305,6 +509,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[binary_sensor.serene_banach_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.serene_banach_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_serene_banach_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.serene_banach_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'serene_banach Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.serene_banach_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.serene_banach_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -356,6 +611,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[binary_sensor.stoic_turing_out_of_memory-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.stoic_turing_out_of_memory', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Out of memory', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Out of memory', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container_out_of_memory', + 'unique_id': 'portainer_test_entry_123_stoic_turing_container_out_of_memory', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.stoic_turing_out_of_memory-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'stoic_turing Out of memory', + }), + 'context': , + 'entity_id': 'binary_sensor.stoic_turing_out_of_memory', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[binary_sensor.stoic_turing_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([