Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions homeassistant/components/portainer/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
from typing import Any

from pyportainer import Portainer
Expand All @@ -30,7 +31,7 @@
PortainerCoordinator,
PortainerCoordinatorData,
)
from .entity import PortainerContainerEntity
from .entity import PortainerContainerEntity, PortainerEndpointEntity


@dataclass(frozen=True, kw_only=True)
Expand All @@ -43,10 +44,24 @@ class PortainerButtonDescription(ButtonEntityDescription):
]


BUTTONS: tuple[PortainerButtonDescription, ...] = (
ENDPOINT_BUTTONS: tuple[PortainerButtonDescription, ...] = (
PortainerButtonDescription(
key="images_prune",
translation_key="images_prune",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_action=(
lambda portainer, endpoint_id, _: portainer.images_prune(
endpoint_id=endpoint_id, dangling=False, until=timedelta(days=0)
)
),
),
)

CONTAINER_BUTTONS: tuple[PortainerButtonDescription, ...] = (
PortainerButtonDescription(
key="restart",
name="Restart Container",
translation_key="restart_container",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_action=(
Expand All @@ -66,22 +81,43 @@ async def async_setup_entry(
"""Set up Portainer buttons."""
coordinator = entry.runtime_data

def _async_add_new_endpoints(endpoints: list[PortainerCoordinatorData]) -> None:
"""Add new endpoint binary sensors."""
async_add_entities(
PortainerEndpointButton(
coordinator,
entity_description,
endpoint,
)
for entity_description in ENDPOINT_BUTTONS
for endpoint in endpoints
)

def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, PortainerContainerData]],
) -> None:
"""Add new container button sensors."""
async_add_entities(
PortainerButton(
PortainerContainerButton(
coordinator,
entity_description,
container,
endpoint,
)
for (endpoint, container) in containers
for entity_description in BUTTONS
for entity_description in CONTAINER_BUTTONS
)

coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints)
coordinator.new_containers_callbacks.append(_async_add_new_containers)

_async_add_new_endpoints(
[
endpoint
for endpoint in coordinator.data.values()
if endpoint.id in coordinator.known_endpoints
]
)
_async_add_new_containers(
[
(endpoint, container)
Expand All @@ -91,7 +127,50 @@ def _async_add_new_containers(
)


class PortainerButton(PortainerContainerEntity, ButtonEntity):
class PortainerEndpointButton(PortainerEndpointEntity, ButtonEntity):
"""Defines a Portainer endpoint button."""

entity_description: PortainerButtonDescription

def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerButtonDescription,
device_info: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer endpoint button entity."""
self.entity_description = entity_description
super().__init__(device_info, coordinator)

self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"

async def async_press(self) -> None:
"""Trigger the Portainer button press service."""
try:
await self.entity_description.press_action(
self.coordinator.portainer, self.device_id, ""
)
except PortainerConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"error": repr(err)},
) from err
except PortainerAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except PortainerTimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_connect",
translation_placeholders={"error": repr(err)},
) from err


class PortainerContainerButton(PortainerContainerEntity, ButtonEntity):
"""Defines a Portainer button."""

entity_description: PortainerButtonDescription
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/portainer/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
"name": "Status"
}
},
"button": {
"images_prune": {
"name": "Prune unused images"
},
"prune_endpoint": {
"name": "Prune endpoint"
},
"restart_container": {
"name": "Restart container"
}
},
"sensor": {
"api_version": {
"name": "API version"
Expand Down
1 change: 1 addition & 0 deletions tests/components/portainer/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def mock_portainer_client() -> Generator[AsyncMock]:
)

client.restart_container = AsyncMock(return_value=None)
client.images_prune = AsyncMock(return_value=None)

yield client

Expand Down
79 changes: 64 additions & 15 deletions tests/components/portainer/snapshots/test_button.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart Container',
'original_name': 'Restart container',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'restart_container',
'unique_id': 'portainer_test_entry_123_focused_einstein_restart',
'unit_of_measurement': None,
})
Expand All @@ -38,7 +38,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'focused_einstein Restart Container',
'friendly_name': 'focused_einstein Restart container',
}),
'context': <ANY>,
'entity_id': 'button.focused_einstein_restart_container',
Expand Down Expand Up @@ -73,12 +73,12 @@
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart Container',
'original_name': 'Restart container',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'restart_container',
'unique_id': 'portainer_test_entry_123_funny_chatelet_restart',
'unit_of_measurement': None,
})
Expand All @@ -87,7 +87,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'funny_chatelet Restart Container',
'friendly_name': 'funny_chatelet Restart container',
}),
'context': <ANY>,
'entity_id': 'button.funny_chatelet_restart_container',
Expand All @@ -97,6 +97,55 @@
'state': 'unknown',
})
# ---
# name: test_all_button_entities_snapshot[button.my_environment_prune_unused_images-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.my_environment_prune_unused_images',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Prune unused images',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'images_prune',
'unique_id': 'portainer_test_entry_123_1_images_prune',
'unit_of_measurement': None,
})
# ---
# name: test_all_button_entities_snapshot[button.my_environment_prune_unused_images-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'my-environment Prune unused images',
}),
'context': <ANY>,
'entity_id': 'button.my_environment_prune_unused_images',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_button_entities_snapshot[button.practical_morse_restart_container-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand All @@ -122,12 +171,12 @@
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart Container',
'original_name': 'Restart container',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'restart_container',
'unique_id': 'portainer_test_entry_123_practical_morse_restart',
'unit_of_measurement': None,
})
Expand All @@ -136,7 +185,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'practical_morse Restart Container',
'friendly_name': 'practical_morse Restart container',
}),
'context': <ANY>,
'entity_id': 'button.practical_morse_restart_container',
Expand Down Expand Up @@ -171,12 +220,12 @@
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart Container',
'original_name': 'Restart container',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'restart_container',
'unique_id': 'portainer_test_entry_123_serene_banach_restart',
'unit_of_measurement': None,
})
Expand All @@ -185,7 +234,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'serene_banach Restart Container',
'friendly_name': 'serene_banach Restart container',
}),
'context': <ANY>,
'entity_id': 'button.serene_banach_restart_container',
Expand Down Expand Up @@ -220,12 +269,12 @@
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart Container',
'original_name': 'Restart container',
'platform': 'portainer',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'restart_container',
'unique_id': 'portainer_test_entry_123_stoic_turing_restart',
'unit_of_measurement': None,
})
Expand All @@ -234,7 +283,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'stoic_turing Restart Container',
'friendly_name': 'stoic_turing Restart container',
}),
'context': <ANY>,
'entity_id': 'button.stoic_turing_restart_container',
Expand Down
Loading
Loading