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:
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user