Portainer add dynamic devices (#155304)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Erwin Douna
2025-10-28 20:57:28 +01:00
committed by GitHub
parent 404393d6fe
commit 09ac47b35f
5 changed files with 135 additions and 35 deletions

View File

@@ -61,30 +61,53 @@ async def async_setup_entry(
) -> None:
"""Set up Portainer binary sensors."""
coordinator = entry.runtime_data
entities: list[BinarySensorEntity] = []
for endpoint in coordinator.data.values():
entities.extend(
def _async_add_new_endpoints(endpoints: list[PortainerCoordinatorData]) -> None:
"""Add new endpoint binary sensors."""
async_add_entities(
PortainerEndpointSensor(
coordinator,
entity_description,
endpoint,
)
for entity_description in ENDPOINT_SENSORS
for endpoint in endpoints
if entity_description.state_fn(endpoint)
)
entities.extend(
def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, DockerContainer]],
) -> None:
"""Add new container binary sensors."""
async_add_entities(
PortainerContainerSensor(
coordinator,
entity_description,
container,
endpoint,
)
for container in endpoint.containers.values()
for (endpoint, container) in containers
for entity_description in CONTAINER_SENSORS
if entity_description.state_fn(container)
)
async_add_entities(entities)
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)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
]
)
class PortainerEndpointSensor(PortainerEndpointEntity, BinarySensorEntity):

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any
from pyportainer import Portainer
@@ -30,8 +29,6 @@ from .const import DOMAIN
from .coordinator import PortainerCoordinator, PortainerCoordinatorData
from .entity import PortainerContainerEntity
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class PortainerButtonDescription(ButtonEntityDescription):
@@ -64,18 +61,30 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Portainer buttons."""
coordinator: PortainerCoordinator = entry.runtime_data
coordinator = entry.runtime_data
async_add_entities(
PortainerButton(
coordinator=coordinator,
entity_description=entity_description,
device_info=container,
via_device=endpoint,
def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, DockerContainer]],
) -> None:
"""Add new container button sensors."""
async_add_entities(
PortainerButton(
coordinator,
entity_description,
container,
endpoint,
)
for (endpoint, container) in containers
for entity_description in BUTTONS
)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
for entity_description in BUTTONS
coordinator.new_containers_callbacks.append(_async_add_new_containers)
_async_add_new_containers(
[
(endpoint, container)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
]
)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
import logging
@@ -64,6 +65,16 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
)
self.portainer = portainer
self.known_endpoints: set[int] = set()
self.known_containers: set[tuple[int, str]] = set()
self.new_endpoints_callbacks: list[
Callable[[list[PortainerCoordinatorData]], None]
] = []
self.new_containers_callbacks: list[
Callable[[list[tuple[PortainerCoordinatorData, DockerContainer]]], None]
] = []
async def _async_setup(self) -> None:
"""Set up the Portainer Data Update Coordinator."""
try:
@@ -152,4 +163,27 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
docker_info=docker_info,
)
self._async_add_remove_endpoints(mapped_endpoints)
return mapped_endpoints
def _async_add_remove_endpoints(
self, mapped_endpoints: dict[int, PortainerCoordinatorData]
) -> None:
"""Add new endpoints, remove non-existing endpoints."""
current_endpoints = {endpoint.id for endpoint in mapped_endpoints.values()}
new_endpoints = current_endpoints - self.known_endpoints
if new_endpoints:
_LOGGER.debug("New endpoints found: %s", new_endpoints)
self.known_endpoints.update(new_endpoints)
# Surprise, we also handle containers here :)
current_containers = {
(endpoint.id, container.id)
for endpoint in mapped_endpoints.values()
for container in endpoint.containers.values()
}
new_containers = current_containers - self.known_containers
if new_containers:
_LOGGER.debug("New containers found: %s", new_containers)
self.known_containers.update(new_containers)

View File

@@ -159,30 +159,53 @@ async def async_setup_entry(
) -> None:
"""Set up Portainer sensors based on a config entry."""
coordinator = entry.runtime_data
entities: list[SensorEntity] = []
for endpoint in coordinator.data.values():
entities.extend(
def _async_add_new_endpoints(endpoints: list[PortainerCoordinatorData]) -> None:
"""Add new endpoint sensor."""
async_add_entities(
PortainerEndpointSensor(
coordinator,
entity_description,
endpoint,
)
for entity_description in ENDPOINT_SENSORS
for endpoint in endpoints
if entity_description.value_fn(endpoint)
)
entities.extend(
def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, DockerContainer]],
) -> None:
"""Add new container sensors."""
async_add_entities(
PortainerContainerSensor(
coordinator,
entity_description,
container,
endpoint,
)
for container in endpoint.containers.values()
for (endpoint, container) in containers
for entity_description in CONTAINER_SENSORS
if entity_description.value_fn(container)
)
async_add_entities(entities)
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)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
]
)
class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):

View File

@@ -85,19 +85,30 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Portainer switch sensors."""
coordinator = entry.runtime_data
async_add_entities(
PortainerContainerSwitch(
coordinator=coordinator,
entity_description=entity_description,
device_info=container,
via_device=endpoint,
def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, DockerContainer]],
) -> None:
"""Add new container switch sensors."""
async_add_entities(
PortainerContainerSwitch(
coordinator,
entity_description,
container,
endpoint,
)
for (endpoint, container) in containers
for entity_description in SWITCHES
)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
for entity_description in SWITCHES
coordinator.new_containers_callbacks.append(_async_add_new_containers)
_async_add_new_containers(
[
(endpoint, container)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
]
)