Skip to content

Commit

Permalink
Merge pull request #5 from jayakornk/main
Browse files Browse the repository at this point in the history
Rewrite and improve functionalities
  • Loading branch information
m1ckyb authored Jul 3, 2022
2 parents d236800 + 3a0ecb3 commit fab2a2b
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 121 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@ This integration is temporary and is expected to soon be merged into HomeAssista
Install using HACS and setup like any other integration via the UI. Enter the uptime kuma instane url and credentials.

The URL needs to be the full url with HTTP://HOST:PORT or HTTPS://HOST

## Issues

Note: Currently, the instance doesn't list the entities on the integrations page, but they should exist under developer tools using the endity id of: `binary_sensor.<uptimekuma monitor name>`.
130 changes: 96 additions & 34 deletions custom_components/uptime_kuma/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""The Uptime Kuma integration."""
from datetime import timedelta
import logging
from __future__ import annotations

import async_timeout
from uptime_kuma_monitor import UptimeKumaMonitor
from pyuptimekuma import (
UptimeKuma,
UptimeKumaConnectionException,
UptimeKumaException,
UptimeKumaMonitor,
)

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
Expand All @@ -15,39 +17,37 @@
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import update_coordinator

from .const import DOMAIN

LOGGER = logging.getLogger(__name__)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

PLATFORMS = [BINARY_SENSOR_DOMAIN]
from .const import COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLATFORMS


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Uptime Kuma integration."""

async def async_get_uptime_kuma_data():
with async_timeout.timeout(10):
utkm = await hass.async_add_executor_job(
UptimeKumaMonitor,
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}",
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_VERIFY_SSL],
)
return utkm.data

coordinator = update_coordinator.DataUpdateCoordinator(
"""Set up Uptime Kuma from a config entry."""
hass.data.setdefault(DOMAIN, {})
host: str = entry.data[CONF_HOST]
port: int = entry.data[CONF_PORT]
username: str = entry.data[CONF_USERNAME]
password: str = entry.data[CONF_PASSWORD]
verify_ssl: bool = entry.data[CONF_VERIFY_SSL]
uptime_kuma_api = UptimeKuma(
async_get_clientsession(hass),
f"{host}:{port}",
username,
password,
verify_ssl,
)
dev_reg = dr.async_get(hass)
hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeKumaDataUpdateCoordinator(
hass,
LOGGER,
name=DOMAIN,
update_method=async_get_uptime_kuma_data,
update_interval=timedelta(minutes=1),
config_entry_id=entry.entry_id,
dev_reg=dev_reg,
api=uptime_kuma_api,
)
await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN] = coordinator
await coordinator.async_config_entry_first_refresh()

hass.config_entries.async_setup_platforms(entry, PLATFORMS)

Expand All @@ -56,8 +56,70 @@ async def async_get_uptime_kuma_data():

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN]
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


class UptimeKumaDataUpdateCoordinator(DataUpdateCoordinator):
"""Data update coordinator for Uptime Kuma"""

data: list[UptimeKumaMonitor]
config_entry: ConfigEntry

def __init__(
self,
hass: HomeAssistant,
config_entry_id: str,
dev_reg: dr.DeviceRegistry,
api: UptimeKuma,
) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=COORDINATOR_UPDATE_INTERVAL,
)
self._config_entry_id = config_entry_id
self._device_registry = dev_reg
self.api = api

async def _async_update_data(self) -> dict | None:
"""Update data."""
try:
response = await self.api.async_get_monitors()
except UptimeKumaConnectionException as exception:
raise UpdateFailed(exception) from exception
except UptimeKumaException as exception:
raise UpdateFailed(exception) from exception

monitors: list[UptimeKumaMonitor] = response.data

current_monitors = {
list(device.identifiers)[0][1]
for device in dr.async_entries_for_config_entry(
self._device_registry, self._config_entry_id
)
}
new_monitors = {str(monitor.monitor_name) for monitor in monitors}
if stale_monitors := current_monitors - new_monitors:
for monitor_id in stale_monitors:
if device := self._device_registry.async_get_device(
{(DOMAIN, monitor_id)}
):
self._device_registry.async_remove_device(device.id)

# If there are new monitors, we should reload the config entry so we can
# create new devices and entities.
if self.data and new_monitors - {
str(monitor.monitor_name) for monitor in self.data
}:
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._config_entry_id)
)
return None

return monitors
# return await super()._async_update_data()
64 changes: 41 additions & 23 deletions custom_components/uptime_kuma/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
"""Summary binary data from Uptime Kuma."""
from voluptuous.validators import Boolean
"""UptimeKuma binary_sensor platform."""
from __future__ import annotations

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from pyuptimekuma import UptimeKumaMonitor

from . import UptimeKumaDataUpdateCoordinator
from .const import DOMAIN
from .entity import UptimeKumaEntity
from .utils import format_entity_name


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Defer sensor setup to the shared sensor module."""
coordinator = hass.data[DOMAIN]
"""Set up the UptimeKuma binary_sensors."""
coordinator: UptimeKumaDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
UptimeKumaBinarySensor(coordinator, monitor) for monitor in coordinator.data
UptimeKumaBinarySensor(
coordinator,
BinarySensorEntityDescription(
key=str(monitor.monitor_name),
name=monitor.monitor_name,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
monitor=monitor,
)
for monitor in coordinator.data
)


class UptimeKumaBinarySensor(BinarySensorEntity, CoordinatorEntity):
"""Represents an Uptime Kuma binary sensor."""

_attr_icon = "mdi:cloud"

def __init__(self, coordinator: DataUpdateCoordinator, monitor: str) -> None:
"""Initialize the Uptime Kuma binary sensor."""
super().__init__(coordinator)
class UptimeKumaBinarySensor(UptimeKumaEntity, BinarySensorEntity):
"""Representation of a UptimeKuma binary sensor."""

self._attr_name = monitor
def __init__(
self,
coordinator: UptimeKumaDataUpdateCoordinator,
description: EntityDescription,
monitor: UptimeKumaMonitor,
) -> None:
"""Set entity ID."""
super().__init__(coordinator, description, monitor)
self.entity_id = (
f"binary_sensor.uptimekuma_{format_entity_name(self.monitor.monitor_name)}"
)

@property
def is_on(self) -> Boolean:
"""Return true if the binary sensor is on."""
return self.coordinator.data[self.name]["monitor_status"] == 1.0
def is_on(self) -> bool:
"""Return True if the entity is on."""
return self.monitor_available
95 changes: 52 additions & 43 deletions custom_components/uptime_kuma/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Config flow to configure the Uptime Kuma integration."""
from uptime_kuma_monitor import UptimeKumaError, UptimeKumaMonitor
"""Config flow for Uptime Kuma integration."""
from __future__ import annotations

from typing import Any

from pyuptimekuma import UptimeKuma, UptimeKumaException
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
Expand All @@ -11,8 +15,9 @@
CONF_VERIFY_SSL,
)
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN
from .const import DOMAIN, LOGGER

STEP_USER_DATA_SCHEMA = vol.Schema(
{
Expand All @@ -25,54 +30,58 @@
)


class UptimeKumaFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle an Uptime Kuma config flow."""
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Uptime Kuma."""

VERSION = 1

async def _show_setup_form(self, errors: dict = None) -> FlowResult:
"""Show the setup form to the user."""

return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors or {},
)

async def async_step_user(self, user_input: dict = None) -> FlowResult:
"""Handle a flow initiated by the user."""
if user_input is None:
return await self._show_setup_form(user_input)
async def _validate_input(
self, data: dict[str, Any]
) -> tuple[dict[str, str], None]:
"""Validate the user input allows us to connect."""
errors: dict[str, str] = {}
host: str = data[CONF_HOST]
port: int = data[CONF_PORT]
username: str = data[CONF_USERNAME]
password: str = data[CONF_PASSWORD]
verify_ssl: bool = data[CONF_VERIFY_SSL]

self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)

errors = {}

username = user_input.get(CONF_USERNAME)
password = user_input.get(CONF_PASSWORD)
utkm = await self.hass.async_add_executor_job(
UptimeKumaMonitor,
f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}",
uptime_robot_api = UptimeKuma(
async_get_clientsession(self.hass),
f"{host}:{port}",
username,
password,
user_input[CONF_VERIFY_SSL],
verify_ssl,
)

try:
await self.hass.async_add_executor_job(utkm.update)
except UptimeKumaError:
await uptime_robot_api.async_get_monitors()
except UptimeKumaException as exception:
LOGGER.error(exception)
errors["base"] = "cannot_connect"
return await self._show_setup_form(errors)
except Exception as exception: # pylint: disable=broad-except
LOGGER.exception(exception)
errors["base"] = "unknown"
# return await self._show_setup_form(errors)

return self.async_create_entry(
title=user_input[CONF_HOST],
data={
CONF_HOST: user_input[CONF_HOST],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_PORT: user_input[CONF_PORT],
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
},
return errors

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)

errors = await self._validate_input(user_input)
if not errors:
unique_id = f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=unique_id, data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
17 changes: 17 additions & 0 deletions custom_components/uptime_kuma/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""Constants for the Uptime Kuma integration."""
from __future__ import annotations

from datetime import timedelta
from logging import Logger, getLogger
from typing import Final

from homeassistant.const import Platform

LOGGER: Logger = getLogger(__package__)

# The free plan is limited to 10 requests/minute
COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10)

DOMAIN = "uptime_kuma"
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR]

ATTRIBUTION: Final = "Data provided by Uptime Kuma"

API_ATTR_OK: Final = "ok"
Loading

0 comments on commit fab2a2b

Please sign in to comment.