Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Portainer integration #129438

Draft
wants to merge 31 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
75ca092
Add Portainer integration
Thomas55555 Sep 17, 2024
c9b0cd2
get token from storage
Thomas55555 Sep 20, 2024
b8daeac
More entities
Thomas55555 Sep 20, 2024
04292ba
Inherit base entity
Thomas55555 Sep 20, 2024
2ad4ea4
.
Thomas55555 Oct 20, 2024
e0b21a0
.
Thomas55555 Oct 20, 2024
1d24713
add port
Thomas55555 Oct 21, 2024
87522d5
.
Thomas55555 Oct 25, 2024
cd6867e
cleanup
Thomas55555 Oct 27, 2024
e8bf2a7
tests
Thomas55555 Oct 27, 2024
220b3ac
Merge branch 'home-assistant:dev' into portainer
Thomas55555 Oct 28, 2024
9abc418
some tests
Thomas55555 Oct 28, 2024
220057a
use pypi
Thomas55555 Oct 28, 2024
6fc954a
mypy
Thomas55555 Oct 29, 2024
c76baf3
100% config flow
Thomas55555 Oct 29, 2024
e1877a8
cleanup
Thomas55555 Oct 29, 2024
ebc783f
.
Thomas55555 Oct 29, 2024
7fc628c
fix entities not updating
Thomas55555 Oct 29, 2024
b904c51
more cleanup
Thomas55555 Nov 2, 2024
a01a67c
dynamically create entites
Thomas55555 Nov 4, 2024
0515c3f
bump to aiotainer 0.0.1b3
Thomas55555 Nov 13, 2024
7d4b7f8
use url instead of host/port
Thomas55555 Nov 13, 2024
e086e5d
add configuration_url
Thomas55555 Nov 14, 2024
f90bef3
add iqs
Thomas55555 Nov 23, 2024
d6828e4
fix quality_scale
Thomas55555 Nov 23, 2024
ab8ed01
bump to aiotainer==0.0.1b5
Thomas55555 Nov 25, 2024
7cad1c8
Merge branch 'dev' into portainer
Thomas55555 Nov 25, 2024
1b7a312
Merge branch 'dev' into portainer
Thomas55555 Dec 8, 2024
cb9e689
Merge branch 'dev' into portainer
Thomas55555 Feb 16, 2025
2d35cbe
update
Thomas55555 Feb 23, 2025
6c6a6c0
remove abstractauth
Thomas55555 Feb 24, 2025
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
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions homeassistant/components/portainer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""The Portainer integration."""

import logging

from aiotainer.auth import Auth
from aiotainer.client import PortainerClient

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_URL, CONF_VERIFY_SSL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client

from .coordinator import PortainerDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [
Platform.SENSOR,
]


type PortainerConfigEntry = ConfigEntry[PortainerDataUpdateCoordinator]


async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
"""Set up this integration using UI."""
client_session = aiohttp_client.async_get_clientsession(
hass, entry.data[CONF_VERIFY_SSL]
)
auth = Auth(client_session, entry.data[CONF_URL], entry.data[CONF_ACCESS_TOKEN])
portainer_api = PortainerClient(auth)
coordinator = PortainerDataUpdateCoordinator(hass, portainer_api)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
"""Handle unload of an entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
56 changes: 56 additions & 0 deletions homeassistant/components/portainer/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""The config_flow for Portainer API integration."""

from typing import Any

from aiohttp.client_exceptions import ClientConnectionError
from aiotainer.auth import Auth
from aiotainer.client import PortainerClient
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv

from .const import DOMAIN

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): cv.string,
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Required(CONF_VERIFY_SSL, default=True): bool,
}
)


class PortainerFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Portainer."""

VERSION = 1

async def async_step_user(
self, user_input: dict[Any, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
websession = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL])
auth = Auth(websession, user_input[CONF_URL], user_input[CONF_ACCESS_TOKEN])
api = PortainerClient(auth)
try:
await api.get_status()
except (TimeoutError, ClientConnectionError):
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(user_input[CONF_URL])
self._abort_if_unique_id_configured()
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An URL is not a good unique id, can we find something unique in the API like an (unique) user id? Otherwise we need to use _abort_entries_match

return self.async_create_entry(
title=user_input[CONF_URL],
data=user_input,
)

return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
)
4 changes: 4 additions & 0 deletions homeassistant/components/portainer/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""The constants for the Portainer integration."""

DOMAIN = "portainer"
NAME = "Portainer"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is unused i believe

38 changes: 38 additions & 0 deletions homeassistant/components/portainer/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Data UpdateCoordinator for the Portainer integration."""

from datetime import timedelta
import logging

from aiotainer.client import PortainerClient
from aiotainer.exceptions import ApiException
from aiotainer.model import NodeData

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(seconds=30)


class PortainerDataUpdateCoordinator(DataUpdateCoordinator[dict[int, NodeData]]):
"""Class to manage fetching data."""

def __init__(self, hass: HomeAssistant, api: PortainerClient) -> None:
"""Initialize data updater."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.api = api

async def _async_update_data(self) -> dict[int, NodeData]:
"""Subscribe for websocket and poll data from the API."""
try:
return await self.api.get_status()
except ApiException as err:
raise UpdateFailed(err) from err
59 changes: 59 additions & 0 deletions homeassistant/components/portainer/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Platform for Portainer base entity."""

import logging
from typing import TYPE_CHECKING

from aiotainer.model import Container, NodeData, Snapshot

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import PortainerDataUpdateCoordinator
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

type PortainerConfigEntry = ConfigEntry[PortainerDataUpdateCoordinator]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already declared in init, delete one of those 2



class ContainerBaseEntity(CoordinatorEntity[PortainerDataUpdateCoordinator]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a Portainer?

"""Defining the Portainer base Entity."""

_attr_has_entity_name = True

def __init__(
self,
coordinator: PortainerDataUpdateCoordinator,
node_id: int,
container_id: str,
) -> None:
"""Initialize PortainerEntity."""
super().__init__(coordinator)
self.node_id = node_id
self.container_id = container_id
if TYPE_CHECKING:
assert coordinator.config_entry is not None
Comment on lines +35 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, overwrite the type in the coordinator

entry: PortainerConfigEntry = coordinator.config_entry
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(node_id))},
name=self.node_attributes.name,
configuration_url=entry.data["url"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
configuration_url=entry.data["url"],
configuration_url=entry.data[CONF_URL],

)

@property
def node_attributes(self) -> NodeData:
"""Get the node attributes of the current node."""
return self.coordinator.data[self.node_id]

@property
def snapshot_attributes(self) -> Snapshot:
"""Get latest snapshot attributes."""
return self.node_attributes.snapshots[-1]

@property
def container_attributes(self) -> Container:
"""Get the container attributes of the current container."""
return self.snapshot_attributes.docker_snapshot_raw.containers[
self.container_id
]
Comment on lines +44 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add an available property that checks that the node_id is known and the container is known as well

9 changes: 9 additions & 0 deletions homeassistant/components/portainer/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"container_state": {
"default": "mdi:state-machine"
}
}
}
}
12 changes: 12 additions & 0 deletions homeassistant/components/portainer/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"domain": "portainer",
"name": "Portainer",
"codeowners": ["@Thomas55555"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/portainer",
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["aiotainer"],
"quality_scale": "bronze",
"requirements": ["aiotainer==0.0.1b6"]
}
82 changes: 82 additions & 0 deletions homeassistant/components/portainer/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
rules:
# Bronze
action-setup:
status: exempt
comment: no actions/services are implemented
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: no actions/services are implemented
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: no subscription required, only polling
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: no actions/services are implemented
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: no configuration options
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates:
status: todo
comment: not set at the moment, we use a coordinator
reauthentication-flow: todo
test-coverage: todo

# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: no discovery possible
discovery:
status: exempt
comment: no discovery possible
docs-data-update: todo
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: no lesspopular entities in this integration
entity-translations:
status: exempt
comment: Entities just have the name of the container which is not translatable
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices: todo

# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo
Loading
Loading