mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add update platform to Uptime Kuma (#148973)
This commit is contained in:
parent
0e9ced3c00
commit
dac75d1902
@ -2,16 +2,37 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pythonkuma.update import UpdateChecker
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .coordinator import UptimeKumaConfigEntry, UptimeKumaDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
UptimeKumaConfigEntry,
|
||||
UptimeKumaDataUpdateCoordinator,
|
||||
UptimeKumaSoftwareUpdateCoordinator,
|
||||
)
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
UPTIME_KUMA_KEY: HassKey[UptimeKumaSoftwareUpdateCoordinator] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool:
|
||||
"""Set up Uptime Kuma from a config entry."""
|
||||
if UPTIME_KUMA_KEY not in hass.data:
|
||||
session = async_get_clientsession(hass)
|
||||
update_checker = UpdateChecker(session)
|
||||
|
||||
update_coordinator = UptimeKumaSoftwareUpdateCoordinator(hass, update_checker)
|
||||
await update_coordinator.async_request_refresh()
|
||||
|
||||
hass.data[UPTIME_KUMA_KEY] = update_coordinator
|
||||
|
||||
coordinator = UptimeKumaDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@ -24,4 +45,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: UptimeKumaConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await hass.data[UPTIME_KUMA_KEY].async_shutdown()
|
||||
hass.data.pop(UPTIME_KUMA_KEY)
|
||||
return unload_ok
|
||||
|
@ -6,12 +6,14 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pythonkuma import (
|
||||
UpdateException,
|
||||
UptimeKuma,
|
||||
UptimeKumaAuthenticationException,
|
||||
UptimeKumaException,
|
||||
UptimeKumaMonitor,
|
||||
UptimeKumaVersion,
|
||||
)
|
||||
from pythonkuma.update import LatestRelease, UpdateChecker
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
@ -25,6 +27,9 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL_UPDATES = timedelta(hours=3)
|
||||
|
||||
type UptimeKumaConfigEntry = ConfigEntry[UptimeKumaDataUpdateCoordinator]
|
||||
|
||||
|
||||
@ -45,7 +50,7 @@ class UptimeKumaDataUpdateCoordinator(
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=30),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
session = async_get_clientsession(hass, config_entry.data[CONF_VERIFY_SSL])
|
||||
self.api = UptimeKuma(
|
||||
@ -105,3 +110,28 @@ def async_migrate_entities_unique_ids(
|
||||
registry_entry.entity_id,
|
||||
new_unique_id=f"{registry_entry.config_entry_id}_{monitor.monitor_id!s}_{registry_entry.translation_key}",
|
||||
)
|
||||
|
||||
|
||||
class UptimeKumaSoftwareUpdateCoordinator(DataUpdateCoordinator[LatestRelease]):
|
||||
"""Uptime Kuma coordinator for retrieving update information."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, update_checker: UpdateChecker) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL_UPDATES,
|
||||
)
|
||||
self.update_checker = update_checker
|
||||
|
||||
async def _async_update_data(self) -> LatestRelease:
|
||||
"""Fetch data."""
|
||||
try:
|
||||
return await self.update_checker.latest_release()
|
||||
except UpdateException as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_check_failed",
|
||||
) from e
|
||||
|
@ -106,6 +106,11 @@
|
||||
"port": {
|
||||
"name": "Monitored port"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"update": {
|
||||
"name": "Uptime Kuma version"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
@ -114,6 +119,9 @@
|
||||
},
|
||||
"request_failed_exception": {
|
||||
"message": "Connection to Uptime Kuma failed"
|
||||
},
|
||||
"update_check_failed": {
|
||||
"message": "Failed to check for latest Uptime Kuma update"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
122
homeassistant/components/uptime_kuma/update.py
Normal file
122
homeassistant/components/uptime_kuma/update.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Update platform for the Uptime Kuma integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.components.update import (
|
||||
UpdateEntity,
|
||||
UpdateEntityDescription,
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import UPTIME_KUMA_KEY
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
UptimeKumaConfigEntry,
|
||||
UptimeKumaDataUpdateCoordinator,
|
||||
UptimeKumaSoftwareUpdateCoordinator,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class UptimeKumaUpdate(StrEnum):
|
||||
"""Uptime Kuma update."""
|
||||
|
||||
UPDATE = "update"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: UptimeKumaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up update platform."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
[UptimeKumaUpdateEntity(coordinator, hass.data[UPTIME_KUMA_KEY])]
|
||||
)
|
||||
|
||||
|
||||
class UptimeKumaUpdateEntity(
|
||||
CoordinatorEntity[UptimeKumaDataUpdateCoordinator], UpdateEntity
|
||||
):
|
||||
"""Representation of an update entity."""
|
||||
|
||||
entity_description = UpdateEntityDescription(
|
||||
key=UptimeKumaUpdate.UPDATE,
|
||||
translation_key=UptimeKumaUpdate.UPDATE,
|
||||
)
|
||||
_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: UptimeKumaDataUpdateCoordinator,
|
||||
update_coordinator: UptimeKumaSoftwareUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.update_checker = update_coordinator
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=coordinator.config_entry.title,
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
manufacturer="Uptime Kuma",
|
||||
configuration_url=coordinator.config_entry.data[CONF_URL],
|
||||
sw_version=coordinator.api.version.version,
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.entry_id}_{self.entity_description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str | None:
|
||||
"""Current version."""
|
||||
|
||||
return self.coordinator.api.version.version
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
"""Title of the release."""
|
||||
|
||||
return f"Uptime Kuma {self.update_checker.data.name}"
|
||||
|
||||
@property
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes."""
|
||||
|
||||
return self.update_checker.data.html_url
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str | None:
|
||||
"""Latest version."""
|
||||
|
||||
return self.update_checker.data.tag_name
|
||||
|
||||
async def async_release_notes(self) -> str | None:
|
||||
"""Return the release notes."""
|
||||
return self.update_checker.data.body
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass.
|
||||
|
||||
Register extra update listener for the software update coordinator.
|
||||
"""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.update_checker.async_add_listener(self._handle_coordinator_update)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.update_checker.last_update_success
|
@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
from pythonkuma import MonitorType, UptimeKumaMonitor, UptimeKumaVersion
|
||||
from pythonkuma.models import MonitorStatus
|
||||
from pythonkuma.update import LatestRelease
|
||||
|
||||
from homeassistant.components.uptime_kuma.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
@ -99,3 +100,22 @@ def mock_pythonkuma() -> Generator[AsyncMock]:
|
||||
)
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_update_checker() -> Generator[AsyncMock]:
|
||||
"""Mock Update checker."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.uptime_kuma.UpdateChecker",
|
||||
autospec=True,
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
client.latest_release.return_value = LatestRelease(
|
||||
html_url="https://github.com/louislam/uptime-kuma/releases/tag/2.0.1",
|
||||
name="2.0.1",
|
||||
tag_name="2.0.1",
|
||||
body="**RELEASE_NOTES**",
|
||||
)
|
||||
|
||||
yield client
|
||||
|
61
tests/components/uptime_kuma/snapshots/test_update.ambr
Normal file
61
tests/components/uptime_kuma/snapshots/test_update.ambr
Normal file
@ -0,0 +1,61 @@
|
||||
# serializer version: 1
|
||||
# name: test_update[update.uptime_example_org_uptime_kuma_version-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'update',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'update.uptime_example_org_uptime_kuma_version',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Uptime Kuma version',
|
||||
'platform': 'uptime_kuma',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <UpdateEntityFeature: 16>,
|
||||
'translation_key': <UptimeKumaUpdate.UPDATE: 'update'>,
|
||||
'unique_id': '123456789_update',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_update[update.uptime_example_org_uptime_kuma_version-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'auto_update': False,
|
||||
'display_precision': 0,
|
||||
'entity_picture': 'https://brands.home-assistant.io/_/uptime_kuma/icon.png',
|
||||
'friendly_name': 'uptime.example.org Uptime Kuma version',
|
||||
'in_progress': False,
|
||||
'installed_version': '2.0.0',
|
||||
'latest_version': '2.0.1',
|
||||
'release_summary': None,
|
||||
'release_url': 'https://github.com/louislam/uptime-kuma/releases/tag/2.0.1',
|
||||
'skipped_version': None,
|
||||
'supported_features': <UpdateEntityFeature: 16>,
|
||||
'title': 'Uptime Kuma 2.0.1',
|
||||
'update_percentage': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'update.uptime_example_org_uptime_kuma_version',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
77
tests/components/uptime_kuma/test_update.py
Normal file
77
tests/components/uptime_kuma/test_update.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""Test the Uptime Kuma update platform."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from pythonkuma import UpdateException
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def update_only() -> AsyncGenerator[None]:
|
||||
"""Enable only the update platform."""
|
||||
with patch(
|
||||
"homeassistant.components.uptime_kuma._PLATFORMS",
|
||||
[Platform.UPDATE],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pythonkuma")
|
||||
async def test_update(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the update platform."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
await ws_client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "update/release_notes",
|
||||
"entity_id": "update.uptime_example_org_uptime_kuma_version",
|
||||
}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["result"] == "**RELEASE_NOTES**"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pythonkuma")
|
||||
async def test_update_unavailable(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_update_checker: AsyncMock,
|
||||
) -> None:
|
||||
"""Test update entity unavailable on error."""
|
||||
|
||||
mock_update_checker.latest_release.side_effect = UpdateException
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get("update.uptime_example_org_uptime_kuma_version")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user