Skip to content

Commit 6664135

Browse files
authored
Add Uptime Kuma integration (home-assistant#146393)
1 parent 37ae476 commit 6664135

22 files changed

+1999
-0
lines changed

.strict-typing

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ homeassistant.components.unifiprotect.*
535535
homeassistant.components.upcloud.*
536536
homeassistant.components.update.*
537537
homeassistant.components.uptime.*
538+
homeassistant.components.uptime_kuma.*
538539
homeassistant.components.uptimerobot.*
539540
homeassistant.components.usb.*
540541
homeassistant.components.uvc.*

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""The Uptime Kuma integration."""
2+
3+
from __future__ import annotations
4+
5+
from homeassistant.const import Platform
6+
from homeassistant.core import HomeAssistant
7+
8+
from .coordinator import UptimeKumaConfigEntry, UptimeKumaDataUpdateCoordinator
9+
10+
_PLATFORMS: list[Platform] = [Platform.SENSOR]
11+
12+
13+
async def async_setup_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool:
14+
"""Set up Uptime Kuma from a config entry."""
15+
16+
coordinator = UptimeKumaDataUpdateCoordinator(hass, entry)
17+
await coordinator.async_config_entry_first_refresh()
18+
entry.runtime_data = coordinator
19+
20+
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
21+
22+
return True
23+
24+
25+
async def async_unload_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool:
26+
"""Unload a config entry."""
27+
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Config flow for the Uptime Kuma integration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import Any
7+
8+
from pythonkuma import (
9+
UptimeKuma,
10+
UptimeKumaAuthenticationException,
11+
UptimeKumaException,
12+
)
13+
import voluptuous as vol
14+
from yarl import URL
15+
16+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
17+
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
18+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
19+
from homeassistant.helpers.selector import (
20+
TextSelector,
21+
TextSelectorConfig,
22+
TextSelectorType,
23+
)
24+
25+
from .const import DOMAIN
26+
27+
_LOGGER = logging.getLogger(__name__)
28+
29+
STEP_USER_DATA_SCHEMA = vol.Schema(
30+
{
31+
vol.Required(CONF_URL): TextSelector(
32+
TextSelectorConfig(
33+
type=TextSelectorType.URL,
34+
autocomplete="url",
35+
),
36+
),
37+
vol.Required(CONF_VERIFY_SSL, default=True): bool,
38+
vol.Optional(CONF_API_KEY, default=""): str,
39+
}
40+
)
41+
42+
43+
class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN):
44+
"""Handle a config flow for Uptime Kuma."""
45+
46+
async def async_step_user(
47+
self, user_input: dict[str, Any] | None = None
48+
) -> ConfigFlowResult:
49+
"""Handle the initial step."""
50+
errors: dict[str, str] = {}
51+
if user_input is not None:
52+
url = URL(user_input[CONF_URL])
53+
self._async_abort_entries_match({CONF_URL: url.human_repr()})
54+
55+
session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL])
56+
uptime_kuma = UptimeKuma(session, url, user_input[CONF_API_KEY])
57+
58+
try:
59+
await uptime_kuma.metrics()
60+
except UptimeKumaAuthenticationException:
61+
errors["base"] = "invalid_auth"
62+
except UptimeKumaException:
63+
errors["base"] = "cannot_connect"
64+
except Exception:
65+
_LOGGER.exception("Unexpected exception")
66+
errors["base"] = "unknown"
67+
else:
68+
return self.async_create_entry(
69+
title=url.host or "",
70+
data={**user_input, CONF_URL: url.human_repr()},
71+
)
72+
73+
return self.async_show_form(
74+
step_id="user",
75+
data_schema=self.add_suggested_values_to_schema(
76+
data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
77+
),
78+
errors=errors,
79+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Constants for the Uptime Kuma integration."""
2+
3+
from pythonkuma import MonitorType
4+
5+
DOMAIN = "uptime_kuma"
6+
7+
HAS_CERT = {
8+
MonitorType.HTTP,
9+
MonitorType.KEYWORD,
10+
MonitorType.JSON_QUERY,
11+
}
12+
HAS_URL = HAS_CERT | {MonitorType.REAL_BROWSER}
13+
HAS_PORT = {
14+
MonitorType.PORT,
15+
MonitorType.STEAM,
16+
MonitorType.GAMEDIG,
17+
MonitorType.MQTT,
18+
MonitorType.RADIUS,
19+
MonitorType.SNMP,
20+
MonitorType.SMTP,
21+
}
22+
HAS_HOST = HAS_PORT | {
23+
MonitorType.PING,
24+
MonitorType.TAILSCALE_PING,
25+
MonitorType.DNS,
26+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Coordinator for the Uptime Kuma integration."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import timedelta
6+
import logging
7+
8+
from pythonkuma import (
9+
UptimeKuma,
10+
UptimeKumaAuthenticationException,
11+
UptimeKumaException,
12+
UptimeKumaMonitor,
13+
UptimeKumaVersion,
14+
)
15+
16+
from homeassistant.config_entries import ConfigEntry
17+
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
18+
from homeassistant.core import HomeAssistant, callback
19+
from homeassistant.exceptions import ConfigEntryError
20+
from homeassistant.helpers import entity_registry as er
21+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
22+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
23+
24+
from .const import DOMAIN
25+
26+
_LOGGER = logging.getLogger(__name__)
27+
28+
type UptimeKumaConfigEntry = ConfigEntry[UptimeKumaDataUpdateCoordinator]
29+
30+
31+
class UptimeKumaDataUpdateCoordinator(
32+
DataUpdateCoordinator[dict[str | int, UptimeKumaMonitor]]
33+
):
34+
"""Update coordinator for Uptime Kuma."""
35+
36+
config_entry: UptimeKumaConfigEntry
37+
38+
def __init__(
39+
self, hass: HomeAssistant, config_entry: UptimeKumaConfigEntry
40+
) -> None:
41+
"""Initialize the coordinator."""
42+
43+
super().__init__(
44+
hass,
45+
_LOGGER,
46+
config_entry=config_entry,
47+
name=DOMAIN,
48+
update_interval=timedelta(seconds=30),
49+
)
50+
session = async_get_clientsession(hass, config_entry.data[CONF_VERIFY_SSL])
51+
self.api = UptimeKuma(
52+
session, config_entry.data[CONF_URL], config_entry.data[CONF_API_KEY]
53+
)
54+
self.version: UptimeKumaVersion | None = None
55+
56+
async def _async_update_data(self) -> dict[str | int, UptimeKumaMonitor]:
57+
"""Fetch the latest data from Uptime Kuma."""
58+
59+
try:
60+
metrics = await self.api.metrics()
61+
except UptimeKumaAuthenticationException as e:
62+
raise ConfigEntryError(
63+
translation_domain=DOMAIN,
64+
translation_key="auth_failed_exception",
65+
) from e
66+
except UptimeKumaException as e:
67+
raise UpdateFailed(
68+
translation_domain=DOMAIN,
69+
translation_key="request_failed_exception",
70+
) from e
71+
else:
72+
async_migrate_entities_unique_ids(self.hass, self, metrics)
73+
self.version = self.api.version
74+
75+
return metrics
76+
77+
78+
@callback
79+
def async_migrate_entities_unique_ids(
80+
hass: HomeAssistant,
81+
coordinator: UptimeKumaDataUpdateCoordinator,
82+
metrics: dict[str | int, UptimeKumaMonitor],
83+
) -> None:
84+
"""Migrate unique_ids in the entity registry after updating Uptime Kuma."""
85+
86+
if (
87+
coordinator.version is coordinator.api.version
88+
or int(coordinator.api.version.major) < 2
89+
):
90+
return
91+
92+
entity_registry = er.async_get(hass)
93+
registry_entries = er.async_entries_for_config_entry(
94+
entity_registry, coordinator.config_entry.entry_id
95+
)
96+
97+
for registry_entry in registry_entries:
98+
name = registry_entry.unique_id.removeprefix(
99+
f"{registry_entry.config_entry_id}_"
100+
).removesuffix(f"_{registry_entry.translation_key}")
101+
if monitor := next(
102+
(m for m in metrics.values() if m.monitor_name == name), None
103+
):
104+
entity_registry.async_update_entity(
105+
registry_entry.entity_id,
106+
new_unique_id=f"{registry_entry.config_entry_id}_{monitor.monitor_id!s}_{registry_entry.translation_key}",
107+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"entity": {
3+
"sensor": {
4+
"cert_days_remaining": {
5+
"default": "mdi:certificate"
6+
},
7+
"response_time": {
8+
"default": "mdi:timeline-clock-outline"
9+
},
10+
"status": {
11+
"default": "mdi:lan-connect",
12+
"state": {
13+
"down": "mdi:lan-disconnect",
14+
"pending": "mdi:lan-pending",
15+
"maintenance": "mdi:account-hard-hat-outline"
16+
}
17+
},
18+
"type": {
19+
"default": "mdi:protocol"
20+
},
21+
"url": {
22+
"default": "mdi:web"
23+
},
24+
"hostname": {
25+
"default": "mdi:ip-outline"
26+
},
27+
"port": {
28+
"default": "mdi:ip-outline"
29+
}
30+
}
31+
}
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"domain": "uptime_kuma",
3+
"name": "Uptime Kuma",
4+
"codeowners": ["@tr4nt0r"],
5+
"config_flow": true,
6+
"documentation": "https://www.home-assistant.io/integrations/uptime_kuma",
7+
"iot_class": "cloud_polling",
8+
"loggers": ["pythonkuma"],
9+
"quality_scale": "bronze",
10+
"requirements": ["pythonkuma==0.3.0"]
11+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
rules:
2+
# Bronze
3+
action-setup:
4+
status: exempt
5+
comment: integration has no actions
6+
appropriate-polling: done
7+
brands: done
8+
common-modules: done
9+
config-flow-test-coverage: done
10+
config-flow: done
11+
dependency-transparency: done
12+
docs-actions:
13+
status: exempt
14+
comment: integration has no actions
15+
docs-high-level-description: done
16+
docs-installation-instructions: done
17+
docs-removal-instructions: done
18+
entity-event-setup:
19+
status: exempt
20+
comment: integration has no events
21+
entity-unique-id: done
22+
has-entity-name: done
23+
runtime-data: done
24+
test-before-configure: done
25+
test-before-setup: done
26+
unique-config-entry: done
27+
28+
# Silver
29+
action-exceptions:
30+
status: exempt
31+
comment: integration has no actions
32+
config-entry-unloading: done
33+
docs-configuration-parameters:
34+
status: exempt
35+
comment: integration has no options
36+
docs-installation-parameters: done
37+
entity-unavailable: done
38+
integration-owner: done
39+
log-when-unavailable: done
40+
parallel-updates: done
41+
reauthentication-flow: todo
42+
test-coverage: done
43+
44+
# Gold
45+
devices: done
46+
diagnostics: todo
47+
discovery-update-info:
48+
status: exempt
49+
comment: is not locally discoverable
50+
discovery:
51+
status: exempt
52+
comment: is not locally discoverable
53+
docs-data-update: done
54+
docs-examples: todo
55+
docs-known-limitations: done
56+
docs-supported-devices:
57+
status: exempt
58+
comment: integration is a service
59+
docs-supported-functions: done
60+
docs-troubleshooting: todo
61+
docs-use-cases: todo
62+
dynamic-devices: done
63+
entity-category: done
64+
entity-device-class: done
65+
entity-disabled-by-default: done
66+
entity-translations: done
67+
exception-translations: done
68+
icon-translations: done
69+
reconfiguration-flow: todo
70+
repair-issues:
71+
status: exempt
72+
comment: has no repairs
73+
stale-devices: done
74+
75+
# Platinum
76+
async-dependency: done
77+
inject-websession: done
78+
strict-typing: done

0 commit comments

Comments
 (0)