1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Portainer add runtime entities (#166320)

This commit is contained in:
Erwin Douna
2026-03-24 16:22:34 +01:00
committed by GitHub
parent d9df5f1fab
commit 85c9b00035
2 changed files with 123 additions and 1 deletions

View File

@@ -276,10 +276,16 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
) -> None:
"""Add new endpoints, remove non-existing endpoints."""
current_endpoints = {endpoint.id for endpoint in mapped_endpoints.values()}
self.known_endpoints &= current_endpoints
new_endpoints = current_endpoints - self.known_endpoints
if new_endpoints:
_LOGGER.debug("New endpoints found: %s", new_endpoints)
self.known_endpoints.update(new_endpoints)
new_endpoint_data = [
mapped_endpoints[endpoint_id] for endpoint_id in new_endpoints
]
for endpoint_callback in self.new_endpoints_callbacks:
endpoint_callback(new_endpoint_data)
# Surprise, we also handle containers here :)
current_containers = {
@@ -287,10 +293,22 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
for endpoint in mapped_endpoints.values()
for container_name in endpoint.containers
}
# Prune departed containers so a recreated container is detected as new
# and its entity is rebuilt with the fresh (ephemeral) Docker container ID.
self.known_containers &= current_containers
new_containers = current_containers - self.known_containers
if new_containers:
_LOGGER.debug("New containers found: %s", new_containers)
self.known_containers.update(new_containers)
new_container_data = [
(
mapped_endpoints[endpoint_id],
mapped_endpoints[endpoint_id].containers[name],
)
for endpoint_id, name in new_containers
]
for container_callback in self.new_containers_callbacks:
container_callback(new_container_data)
# Stack management
current_stacks = {
@@ -298,10 +316,21 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
for endpoint in mapped_endpoints.values()
for stack_name in endpoint.stacks
}
self.known_stacks &= current_stacks
new_stacks = current_stacks - self.known_stacks
if new_stacks:
_LOGGER.debug("New stacks found: %s", new_stacks)
self.known_stacks.update(new_stacks)
new_stack_data = [
(
mapped_endpoints[endpoint_id],
mapped_endpoints[endpoint_id].stacks[name],
)
for endpoint_id, name in new_stacks
]
for stack_callback in self.new_stacks_callbacks:
stack_callback(new_stack_data)
def _get_container_name(self, container_name: str) -> str:
"""Sanitize to get a proper container name."""

View File

@@ -7,6 +7,9 @@ from pyportainer.exceptions import (
PortainerConnectionError,
PortainerTimeoutError,
)
from pyportainer.models.docker import DockerContainer
from pyportainer.models.portainer import Endpoint
from pyportainer.models.stacks import Stack
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -26,7 +29,7 @@ from homeassistant.setup import async_setup_component
from . import setup_integration
from .conftest import MOCK_TEST_CONFIG, TEST_INSTANCE_ID
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_load_json_array_fixture
from tests.typing import WebSocketGenerator
@@ -296,3 +299,93 @@ async def test_container_stack_device_links(
assert standalone_container_device is not None
assert standalone_container_device.via_device_id == endpoint_device.id
async def test_new_endpoint_callback(
hass: HomeAssistant,
mock_portainer_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that a new endpoint appearing in a subsequent refresh fires the callback and creates entities."""
mock_portainer_client.get_endpoints.return_value = []
await setup_integration(hass, mock_config_entry)
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entities) == 0
mock_portainer_client.get_endpoints.return_value = [
Endpoint.from_dict(endpoint)
for endpoint in await async_load_json_array_fixture(
hass, "endpoints.json", DOMAIN
)
if endpoint["Status"] == 1
]
coordinator = mock_config_entry.runtime_data
await coordinator.async_refresh()
await hass.async_block_till_done()
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entities) > 0
async def test_new_container_callback(
hass: HomeAssistant,
mock_portainer_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that a new container appearing in a subsequent refresh fires the callback and creates entities."""
mock_portainer_client.get_containers.return_value = []
await setup_integration(hass, mock_config_entry)
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
mock_portainer_client.get_containers.return_value = [
DockerContainer.from_dict(container)
for container in await async_load_json_array_fixture(
hass, "containers.json", DOMAIN
)
if "/focused_einstein" in container["Names"]
]
coordinator = mock_config_entry.runtime_data
await coordinator.async_refresh()
await hass.async_block_till_done()
assert len(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
) > len(entities)
async def test_new_stack_callback(
hass: HomeAssistant,
mock_portainer_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that a new stack appearing in a subsequent refresh fires the callback and creates entities."""
mock_portainer_client.get_stacks.return_value = []
await setup_integration(hass, mock_config_entry)
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
mock_portainer_client.get_stacks.return_value = [
Stack.from_dict(stack)
for stack in await async_load_json_array_fixture(hass, "stacks.json", DOMAIN)
if stack["Name"] == "webstack"
]
coordinator = mock_config_entry.runtime_data
await coordinator.async_refresh()
await hass.async_block_till_done()
assert len(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
) > len(entities)