Files
core/homeassistant/components/portainer/switch.py
T

228 lines
7.2 KiB
Python

"""Switch platform for Portainer containers."""
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, override
from pyportainer import DockerContainerState, Portainer, StackStatus
from pyportainer.exceptions import (
PortainerAuthenticationError,
PortainerConnectionError,
PortainerTimeoutError,
)
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PortainerConfigEntry
from .const import DOMAIN
from .coordinator import (
PortainerContainerData,
PortainerCoordinator,
PortainerStackData,
)
from .entity import (
PortainerContainerEntity,
PortainerCoordinatorData,
PortainerStackEntity,
)
@dataclass(frozen=True, kw_only=True)
class PortainerSwitchEntityDescription(SwitchEntityDescription):
"""Class to hold Portainer switch description."""
is_on_fn: Callable[[PortainerContainerData], bool | None]
turn_on_fn: Callable[[Portainer], Callable[[int, str], Coroutine[Any, Any, None]]]
turn_off_fn: Callable[[Portainer], Callable[[int, str], Coroutine[Any, Any, None]]]
@dataclass(frozen=True, kw_only=True)
class PortainerStackSwitchEntityDescription(SwitchEntityDescription):
"""Class to hold Portainer stack switch description."""
is_on_fn: Callable[[PortainerStackData], bool | None]
turn_on_fn: Callable[[Portainer], Callable[..., Coroutine[Any, Any, Any]]]
turn_off_fn: Callable[[Portainer], Callable[..., Coroutine[Any, Any, Any]]]
PARALLEL_UPDATES = 1
async def _perform_action(
coordinator: PortainerCoordinator,
coroutine: Coroutine[Any, Any, Any],
) -> None:
"""Perform a Portainer action with error handling and coordinator refresh."""
try:
await coroutine
except PortainerAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except PortainerConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except PortainerTimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_connect",
) from err
else:
await coordinator.async_request_refresh()
CONTAINER_SWITCHES: tuple[PortainerSwitchEntityDescription, ...] = (
PortainerSwitchEntityDescription(
key="container",
translation_key="container",
device_class=SwitchDeviceClass.SWITCH,
is_on_fn=lambda data: (
data.container.state
in (DockerContainerState.RUNNING, DockerContainerState.PAUSED)
),
turn_on_fn=lambda portainer: portainer.start_container,
turn_off_fn=lambda portainer: portainer.stop_container,
),
)
STACK_SWITCHES: tuple[PortainerStackSwitchEntityDescription, ...] = (
PortainerStackSwitchEntityDescription(
key="stack",
translation_key="stack",
device_class=SwitchDeviceClass.SWITCH,
is_on_fn=lambda data: data.stack.status == StackStatus.ACTIVE,
turn_on_fn=lambda portainer: portainer.start_stack,
turn_off_fn=lambda portainer: portainer.stop_stack,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PortainerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Portainer switch sensors."""
coordinator = entry.runtime_data
def _async_add_new_containers(
containers: list[tuple[PortainerCoordinatorData, PortainerContainerData]],
) -> None:
"""Add new container switch sensors."""
async_add_entities(
PortainerContainerSwitch(
coordinator,
entity_description,
container,
endpoint,
)
for (endpoint, container) in containers
for entity_description in CONTAINER_SWITCHES
)
def _async_add_new_stacks(
stacks: list[tuple[PortainerCoordinatorData, PortainerStackData]],
) -> None:
"""Add new stack switch sensors."""
async_add_entities(
PortainerStackSwitch(
coordinator,
entity_description,
stack,
endpoint,
)
for (endpoint, stack) in stacks
for entity_description in STACK_SWITCHES
)
coordinator.new_containers_callbacks.append(_async_add_new_containers)
coordinator.new_stacks_callbacks.append(_async_add_new_stacks)
_async_add_new_containers(
[
(endpoint, container)
for endpoint in coordinator.data.values()
for container in endpoint.containers.values()
]
)
_async_add_new_stacks(
[
(endpoint, stack)
for endpoint in coordinator.data.values()
for stack in endpoint.stacks.values()
]
)
class PortainerContainerSwitch(PortainerContainerEntity, SwitchEntity):
"""Representation of a Portainer container switch."""
entity_description: PortainerSwitchEntityDescription
@property
@override
def is_on(self) -> bool | None:
"""Return the state of the device."""
return self.entity_description.is_on_fn(self.container_data)
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start (turn on) the container."""
await _perform_action(
self.coordinator,
self.entity_description.turn_on_fn(self.coordinator.portainer)(
self.endpoint_id, self.container_data.container.id
),
)
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop (turn off) the container."""
await _perform_action(
self.coordinator,
self.entity_description.turn_off_fn(self.coordinator.portainer)(
self.endpoint_id, self.container_data.container.id
),
)
class PortainerStackSwitch(PortainerStackEntity, SwitchEntity):
"""Representation of a Portainer stack switch."""
entity_description: PortainerStackSwitchEntityDescription
@property
@override
def is_on(self) -> bool | None:
"""Return the state of the device."""
return self.entity_description.is_on_fn(self.stack_data)
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start (turn on) the stack."""
await _perform_action(
self.coordinator,
self.entity_description.turn_on_fn(self.coordinator.portainer)(
self.endpoint_id, self.stack_data.stack.id
),
)
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop (turn off) the stack."""
await _perform_action(
self.coordinator,
self.entity_description.turn_off_fn(self.coordinator.portainer)(
self.endpoint_id, self.stack_data.stack.id
),
)