Skip to content

Commit 4912280

Browse files
erwindounafrenck
andauthored
Portainer add endoint sensors (home-assistant#154676)
Co-authored-by: Franck Nijhof <[email protected]>
1 parent d4e72ad commit 4912280

File tree

11 files changed

+1082
-25
lines changed

11 files changed

+1082
-25
lines changed

homeassistant/components/portainer/coordinator.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
PortainerTimeoutError,
1414
)
1515
from pyportainer.models.docker import DockerContainer
16+
from pyportainer.models.docker_inspect import DockerInfo, DockerVersion
1617
from pyportainer.models.portainer import Endpoint
1718

1819
from homeassistant.config_entries import ConfigEntry
@@ -38,6 +39,8 @@ class PortainerCoordinatorData:
3839
name: str | None
3940
endpoint: Endpoint
4041
containers: dict[str, DockerContainer]
42+
docker_version: DockerVersion
43+
docker_info: DockerInfo
4144

4245

4346
class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorData]]):
@@ -120,6 +123,8 @@ async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]:
120123

121124
try:
122125
containers = await self.portainer.get_containers(endpoint.id)
126+
docker_version = await self.portainer.docker_version(endpoint.id)
127+
docker_info = await self.portainer.docker_info(endpoint.id)
123128
except PortainerConnectionError as err:
124129
_LOGGER.exception("Connection error")
125130
raise UpdateFailed(
@@ -140,6 +145,8 @@ async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]:
140145
name=endpoint.name,
141146
endpoint=endpoint,
142147
containers={container.id: container for container in containers},
148+
docker_version=docker_version,
149+
docker_info=docker_info,
143150
)
144151

145152
return mapped_endpoints

homeassistant/components/portainer/icons.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@
33
"sensor": {
44
"image": {
55
"default": "mdi:docker"
6+
},
7+
"operating_system": {
8+
"default": "mdi:chip"
9+
},
10+
"operating_system_version": {
11+
"default": "mdi:alpha-v-box"
12+
},
13+
"api_version": {
14+
"default": "mdi:api"
15+
},
16+
"kernel_version": {
17+
"default": "mdi:memory"
18+
},
19+
"architecture": {
20+
"default": "mdi:cpu-64-bit"
21+
},
22+
"containers_running": {
23+
"default": "mdi:play-circle-outline"
24+
},
25+
"containers_stopped": {
26+
"default": "mdi:stop-circle-outline"
27+
},
28+
"containers_paused": {
29+
"default": "mdi:pause-circle"
30+
},
31+
"images_count": {
32+
"default": "mdi:image-multiple"
33+
},
34+
"containers_count": {
35+
"default": "mdi:database"
36+
},
37+
"memory_total": {
38+
"default": "mdi:memory"
39+
},
40+
"docker_version": {
41+
"default": "mdi:docker"
42+
},
43+
"cpu_total": {
44+
"default": "mdi:cpu-64-bit"
645
}
746
},
847
"switch": {

homeassistant/components/portainer/sensor.py

Lines changed: 182 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,149 @@
77

88
from pyportainer.models.docker import DockerContainer
99

10-
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
10+
from homeassistant.components.sensor import (
11+
EntityCategory,
12+
SensorDeviceClass,
13+
SensorEntity,
14+
SensorEntityDescription,
15+
SensorStateClass,
16+
StateType,
17+
)
18+
from homeassistant.const import UnitOfInformation
1119
from homeassistant.core import HomeAssistant
1220
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1321

1422
from .coordinator import PortainerConfigEntry, PortainerCoordinator
15-
from .entity import PortainerContainerEntity, PortainerCoordinatorData
23+
from .entity import (
24+
PortainerContainerEntity,
25+
PortainerCoordinatorData,
26+
PortainerEndpointEntity,
27+
)
28+
29+
30+
@dataclass(frozen=True, kw_only=True)
31+
class PortainerContainerSensorEntityDescription(SensorEntityDescription):
32+
"""Class to hold Portainer container sensor description."""
33+
34+
value_fn: Callable[[DockerContainer], StateType]
1635

1736

1837
@dataclass(frozen=True, kw_only=True)
19-
class PortainerSensorEntityDescription(SensorEntityDescription):
20-
"""Class to hold Portainer sensor description."""
38+
class PortainerEndpointSensorEntityDescription(SensorEntityDescription):
39+
"""Class to hold Portainer endpoint sensor description."""
2140

22-
value_fn: Callable[[DockerContainer], str | None]
41+
value_fn: Callable[[PortainerCoordinatorData], StateType]
2342

2443

25-
CONTAINER_SENSORS: tuple[PortainerSensorEntityDescription, ...] = (
26-
PortainerSensorEntityDescription(
44+
CONTAINER_SENSORS: tuple[PortainerContainerSensorEntityDescription, ...] = (
45+
PortainerContainerSensorEntityDescription(
2746
key="image",
2847
translation_key="image",
2948
value_fn=lambda data: data.image,
3049
),
3150
)
51+
ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
52+
PortainerEndpointSensorEntityDescription(
53+
key="api_version",
54+
translation_key="api_version",
55+
value_fn=lambda data: data.docker_version.api_version,
56+
entity_category=EntityCategory.DIAGNOSTIC,
57+
entity_registry_enabled_default=False,
58+
),
59+
PortainerEndpointSensorEntityDescription(
60+
key="kernel_version",
61+
translation_key="kernel_version",
62+
value_fn=lambda data: data.docker_version.kernel_version,
63+
entity_category=EntityCategory.DIAGNOSTIC,
64+
entity_registry_enabled_default=False,
65+
),
66+
PortainerEndpointSensorEntityDescription(
67+
key="operating_system",
68+
translation_key="operating_system",
69+
value_fn=lambda data: data.docker_info.os_type,
70+
entity_category=EntityCategory.DIAGNOSTIC,
71+
entity_registry_enabled_default=False,
72+
),
73+
PortainerEndpointSensorEntityDescription(
74+
key="operating_system_version",
75+
translation_key="operating_system_version",
76+
value_fn=lambda data: data.docker_info.os_version,
77+
entity_category=EntityCategory.DIAGNOSTIC,
78+
entity_registry_enabled_default=False,
79+
),
80+
PortainerEndpointSensorEntityDescription(
81+
key="docker_version",
82+
translation_key="docker_version",
83+
value_fn=lambda data: data.docker_info.server_version,
84+
entity_category=EntityCategory.DIAGNOSTIC,
85+
entity_registry_enabled_default=False,
86+
),
87+
PortainerEndpointSensorEntityDescription(
88+
key="architecture",
89+
translation_key="architecture",
90+
value_fn=lambda data: data.docker_info.architecture,
91+
entity_category=EntityCategory.DIAGNOSTIC,
92+
entity_registry_enabled_default=False,
93+
),
94+
PortainerEndpointSensorEntityDescription(
95+
key="containers_count",
96+
translation_key="containers_count",
97+
value_fn=lambda data: data.docker_info.containers,
98+
entity_category=EntityCategory.DIAGNOSTIC,
99+
entity_registry_enabled_default=False,
100+
state_class=SensorStateClass.MEASUREMENT,
101+
),
102+
PortainerEndpointSensorEntityDescription(
103+
key="containers_running",
104+
translation_key="containers_running",
105+
value_fn=lambda data: data.docker_info.containers_running,
106+
entity_category=EntityCategory.DIAGNOSTIC,
107+
entity_registry_enabled_default=False,
108+
state_class=SensorStateClass.MEASUREMENT,
109+
),
110+
PortainerEndpointSensorEntityDescription(
111+
key="containers_stopped",
112+
translation_key="containers_stopped",
113+
value_fn=lambda data: data.docker_info.containers_stopped,
114+
entity_category=EntityCategory.DIAGNOSTIC,
115+
entity_registry_enabled_default=False,
116+
state_class=SensorStateClass.MEASUREMENT,
117+
),
118+
PortainerEndpointSensorEntityDescription(
119+
key="containers_paused",
120+
translation_key="containers_paused",
121+
value_fn=lambda data: data.docker_info.containers_paused,
122+
entity_category=EntityCategory.DIAGNOSTIC,
123+
entity_registry_enabled_default=False,
124+
state_class=SensorStateClass.MEASUREMENT,
125+
),
126+
PortainerEndpointSensorEntityDescription(
127+
key="images_count",
128+
translation_key="images_count",
129+
value_fn=lambda data: data.docker_info.images,
130+
entity_category=EntityCategory.DIAGNOSTIC,
131+
entity_registry_enabled_default=False,
132+
state_class=SensorStateClass.MEASUREMENT,
133+
),
134+
PortainerEndpointSensorEntityDescription(
135+
key="memory_total",
136+
translation_key="memory_total",
137+
value_fn=lambda data: data.docker_info.mem_total,
138+
device_class=SensorDeviceClass.DATA_SIZE,
139+
state_class=SensorStateClass.MEASUREMENT,
140+
native_unit_of_measurement=UnitOfInformation.BYTES,
141+
entity_category=EntityCategory.DIAGNOSTIC,
142+
entity_registry_enabled_default=False,
143+
),
144+
PortainerEndpointSensorEntityDescription(
145+
key="cpu_total",
146+
translation_key="cpu_total",
147+
value_fn=lambda data: data.docker_info.ncpu,
148+
entity_category=EntityCategory.DIAGNOSTIC,
149+
entity_registry_enabled_default=False,
150+
state_class=SensorStateClass.MEASUREMENT,
151+
),
152+
)
32153

33154

34155
async def async_setup_entry(
@@ -38,29 +159,41 @@ async def async_setup_entry(
38159
) -> None:
39160
"""Set up Portainer sensors based on a config entry."""
40161
coordinator = entry.runtime_data
162+
entities: list[SensorEntity] = []
163+
164+
for endpoint in coordinator.data.values():
165+
entities.extend(
166+
PortainerEndpointSensor(
167+
coordinator,
168+
entity_description,
169+
endpoint,
170+
)
171+
for entity_description in ENDPOINT_SENSORS
172+
)
41173

42-
async_add_entities(
43-
PortainerContainerSensor(
44-
coordinator,
45-
entity_description,
46-
container,
47-
endpoint,
174+
entities.extend(
175+
PortainerContainerSensor(
176+
coordinator,
177+
entity_description,
178+
container,
179+
endpoint,
180+
)
181+
for container in endpoint.containers.values()
182+
for entity_description in CONTAINER_SENSORS
48183
)
49-
for endpoint in coordinator.data.values()
50-
for container in endpoint.containers.values()
51-
for entity_description in CONTAINER_SENSORS
52-
)
184+
185+
async_add_entities(entities)
53186

54187

55188
class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):
56189
"""Representation of a Portainer container sensor."""
57190

58-
entity_description: PortainerSensorEntityDescription
191+
entity_description: PortainerContainerSensorEntityDescription
59192

60193
def __init__(
61194
self,
62195
coordinator: PortainerCoordinator,
63-
entity_description: PortainerSensorEntityDescription,
196+
entity_description: PortainerContainerSensorEntityDescription,
64197
device_info: DockerContainer,
65198
via_device: PortainerCoordinatorData,
66199
) -> None:
@@ -76,8 +209,37 @@ def available(self) -> bool:
76209
return super().available and self.endpoint_id in self.coordinator.data
77210

78211
@property
79-
def native_value(self) -> str | None:
212+
def native_value(self) -> StateType:
80213
"""Return the state of the sensor."""
81214
return self.entity_description.value_fn(
82215
self.coordinator.data[self.endpoint_id].containers[self.device_id]
83216
)
217+
218+
219+
class PortainerEndpointSensor(PortainerEndpointEntity, SensorEntity):
220+
"""Representation of a Portainer endpoint sensor."""
221+
222+
entity_description: PortainerEndpointSensorEntityDescription
223+
224+
def __init__(
225+
self,
226+
coordinator: PortainerCoordinator,
227+
entity_description: PortainerEndpointSensorEntityDescription,
228+
device_info: PortainerCoordinatorData,
229+
) -> None:
230+
"""Initialize the Portainer endpoint sensor."""
231+
self.entity_description = entity_description
232+
super().__init__(device_info, coordinator)
233+
234+
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
235+
236+
@property
237+
def available(self) -> bool:
238+
"""Return if the device is available."""
239+
return super().available and self.device_id in self.coordinator.data
240+
241+
@property
242+
def native_value(self) -> StateType:
243+
"""Return the state of the sensor."""
244+
endpoint_data = self.coordinator.data[self._device_info.endpoint.id]
245+
return self.entity_description.value_fn(endpoint_data)

homeassistant/components/portainer/strings.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,45 @@
4949
"sensor": {
5050
"image": {
5151
"name": "Image"
52+
},
53+
"operating_system": {
54+
"name": "Operating system"
55+
},
56+
"operating_system_version": {
57+
"name": "Operating system version"
58+
},
59+
"api_version": {
60+
"name": "API version"
61+
},
62+
"kernel_version": {
63+
"name": "Kernel version"
64+
},
65+
"architecture": {
66+
"name": "Architecture"
67+
},
68+
"containers_running": {
69+
"name": "Containers running"
70+
},
71+
"containers_stopped": {
72+
"name": "Containers stopped"
73+
},
74+
"containers_paused": {
75+
"name": "Containers paused"
76+
},
77+
"images_count": {
78+
"name": "Image count"
79+
},
80+
"containers_count": {
81+
"name": "Container count"
82+
},
83+
"memory_total": {
84+
"name": "Total memory"
85+
},
86+
"docker_version": {
87+
"name": "Docker version"
88+
},
89+
"cpu_total": {
90+
"name": "Total CPU"
5291
}
5392
},
5493
"switch": {

0 commit comments

Comments
 (0)