mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Add automatic backup event entity to Home Assistant Backup system (#145350)
* add automatic backup event entity * add tests * fix test * Apply suggestions from code review Co-authored-by: Josef Zweck <josef@zweck.dev> * implement _handle_coordinator_update * add translations for event attributes * simplify condition * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --------- Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
5048d1512c
commit
086e97821f
@ -81,7 +81,7 @@ __all__ = [
|
|||||||
"suggested_filename_from_name_date",
|
"suggested_filename_from_name_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.EVENT, Platform.SENSOR]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ class BackupCoordinatorData:
|
|||||||
last_attempted_automatic_backup: datetime | None
|
last_attempted_automatic_backup: datetime | None
|
||||||
last_successful_automatic_backup: datetime | None
|
last_successful_automatic_backup: datetime | None
|
||||||
next_scheduled_automatic_backup: datetime | None
|
next_scheduled_automatic_backup: datetime | None
|
||||||
|
last_event: ManagerStateEvent | BackupPlatformEvent | None
|
||||||
|
|
||||||
|
|
||||||
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
||||||
@ -60,11 +61,13 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.backup_manager = backup_manager
|
self.backup_manager = backup_manager
|
||||||
|
self._last_event: ManagerStateEvent | BackupPlatformEvent | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
|
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
|
||||||
"""Handle new event."""
|
"""Handle new event."""
|
||||||
LOGGER.debug("Received backup event: %s", event)
|
LOGGER.debug("Received backup event: %s", event)
|
||||||
|
self._last_event = event
|
||||||
self.config_entry.async_create_task(self.hass, self.async_refresh())
|
self.config_entry.async_create_task(self.hass, self.async_refresh())
|
||||||
|
|
||||||
async def _async_update_data(self) -> BackupCoordinatorData:
|
async def _async_update_data(self) -> BackupCoordinatorData:
|
||||||
@ -74,6 +77,7 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
|||||||
self.backup_manager.config.data.last_attempted_automatic_backup,
|
self.backup_manager.config.data.last_attempted_automatic_backup,
|
||||||
self.backup_manager.config.data.last_completed_automatic_backup,
|
self.backup_manager.config.data.last_completed_automatic_backup,
|
||||||
self.backup_manager.config.data.schedule.next_automatic_backup,
|
self.backup_manager.config.data.schedule.next_automatic_backup,
|
||||||
|
self._last_event,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -11,7 +11,7 @@ from .const import DOMAIN
|
|||||||
from .coordinator import BackupDataUpdateCoordinator
|
from .coordinator import BackupDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
class BackupManagerBaseEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
||||||
"""Base entity for backup manager."""
|
"""Base entity for backup manager."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@ -19,12 +19,9 @@ class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: BackupDataUpdateCoordinator,
|
coordinator: BackupDataUpdateCoordinator,
|
||||||
entity_description: EntityDescription,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize base entity."""
|
"""Initialize base entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
|
||||||
self._attr_unique_id = entity_description.key
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, "backup_manager")},
|
identifiers={(DOMAIN, "backup_manager")},
|
||||||
manufacturer="Home Assistant",
|
manufacturer="Home Assistant",
|
||||||
@ -34,3 +31,17 @@ class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
|||||||
entry_type=DeviceEntryType.SERVICE,
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
configuration_url="homeassistant://config/backup",
|
configuration_url="homeassistant://config/backup",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManagerEntity(BackupManagerBaseEntity):
|
||||||
|
"""Entity for backup manager."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BackupDataUpdateCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = entity_description.key
|
||||||
|
59
homeassistant/components/backup/event.py
Normal file
59
homeassistant/components/backup/event.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Event platform for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.components.event import EventEntity
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
|
||||||
|
from .entity import BackupManagerBaseEntity
|
||||||
|
from .manager import CreateBackupEvent, CreateBackupState
|
||||||
|
|
||||||
|
ATTR_BACKUP_STAGE: Final[str] = "backup_stage"
|
||||||
|
ATTR_FAILED_REASON: Final[str] = "failed_reason"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BackupConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Event set up for backup config entry."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
async_add_entities([AutomaticBackupEvent(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class AutomaticBackupEvent(BackupManagerBaseEntity, EventEntity):
|
||||||
|
"""Representation of an automatic backup event."""
|
||||||
|
|
||||||
|
_attr_event_types = [s.value for s in CreateBackupState]
|
||||||
|
_unrecorded_attributes = frozenset({ATTR_FAILED_REASON, ATTR_BACKUP_STAGE})
|
||||||
|
coordinator: BackupDataUpdateCoordinator
|
||||||
|
|
||||||
|
def __init__(self, coordinator: BackupDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the automatic backup event."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_unique_id = "automatic_backup_event"
|
||||||
|
self._attr_translation_key = "automatic_backup_event"
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
if (
|
||||||
|
not (data := self.coordinator.data)
|
||||||
|
or (event := data.last_event) is None
|
||||||
|
or not isinstance(event, CreateBackupEvent)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._trigger_event(
|
||||||
|
event.state,
|
||||||
|
{
|
||||||
|
ATTR_BACKUP_STAGE: event.stage,
|
||||||
|
ATTR_FAILED_REASON: event.reason,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"entity": {
|
||||||
|
"event": {
|
||||||
|
"automatic_backup_event": {
|
||||||
|
"default": "mdi:database"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"create": {
|
"create": {
|
||||||
"service": "mdi:cloud-upload"
|
"service": "mdi:cloud-upload"
|
||||||
|
@ -36,6 +36,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"event": {
|
||||||
|
"automatic_backup_event": {
|
||||||
|
"name": "Automatic backup",
|
||||||
|
"state_attributes": {
|
||||||
|
"event_type": {
|
||||||
|
"state": {
|
||||||
|
"completed": "Completed successfully",
|
||||||
|
"failed": "Failed",
|
||||||
|
"in_progress": "In progress"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backup_stage": { "name": "Backup stage" },
|
||||||
|
"failed_reason": { "name": "Failure reason" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"backup_manager_state": {
|
"backup_manager_state": {
|
||||||
"name": "Backup Manager state",
|
"name": "Backup Manager state",
|
||||||
|
60
tests/components/backup/snapshots/test_event.ambr
Normal file
60
tests/components/backup/snapshots/test_event.ambr
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_event_entity[event.backup_automatic_backup-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'event_types': list([
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'in_progress',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'event',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'event.backup_automatic_backup',
|
||||||
|
'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': 'Automatic backup',
|
||||||
|
'platform': 'backup',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'automatic_backup_event',
|
||||||
|
'unique_id': 'automatic_backup_event',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_event_entity[event.backup_automatic_backup-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'event_type': None,
|
||||||
|
'event_types': list([
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'in_progress',
|
||||||
|
]),
|
||||||
|
'friendly_name': 'Backup Automatic backup',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'event.backup_automatic_backup',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
95
tests/components/backup/test_event.py
Normal file
95
tests/components/backup/test_event.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""The tests for the Backup event entity."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.backup.const import DOMAIN
|
||||||
|
from homeassistant.components.backup.event import ATTR_BACKUP_STAGE, ATTR_FAILED_REASON
|
||||||
|
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from .common import setup_backup_integration
|
||||||
|
|
||||||
|
from tests.common import snapshot_platform
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_backup_generation")
|
||||||
|
async def test_event_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test automatic backup event entity."""
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]):
|
||||||
|
await setup_backup_integration(hass, with_hassio=False)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_backup_generation")
|
||||||
|
async def test_event_entity_backup_completed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test completed automatic backup event."""
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]):
|
||||||
|
await setup_backup_integration(hass, with_hassio=False)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get("event.backup_automatic_backup")
|
||||||
|
assert state.attributes[ATTR_EVENT_TYPE] is None
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{"type": "backup/generate", "agent_ids": ["backup.local"]}
|
||||||
|
)
|
||||||
|
assert await client.receive_json()
|
||||||
|
|
||||||
|
state = hass.states.get("event.backup_automatic_backup")
|
||||||
|
assert state.attributes[ATTR_EVENT_TYPE] == "in_progress"
|
||||||
|
assert state.attributes[ATTR_BACKUP_STAGE] is not None
|
||||||
|
assert state.attributes[ATTR_FAILED_REASON] is None
|
||||||
|
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
state = hass.states.get("event.backup_automatic_backup")
|
||||||
|
assert state.attributes[ATTR_EVENT_TYPE] == "completed"
|
||||||
|
assert state.attributes[ATTR_BACKUP_STAGE] is None
|
||||||
|
assert state.attributes[ATTR_FAILED_REASON] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_backup_generation")
|
||||||
|
async def test_event_entity_backup_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
create_backup: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test failed automatic backup event."""
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]):
|
||||||
|
await setup_backup_integration(hass, with_hassio=False)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get("event.backup_automatic_backup")
|
||||||
|
assert state.attributes[ATTR_EVENT_TYPE] is None
|
||||||
|
|
||||||
|
create_backup.side_effect = Exception("Boom!")
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{"type": "backup/generate", "agent_ids": ["backup.local"]}
|
||||||
|
)
|
||||||
|
assert await client.receive_json()
|
||||||
|
|
||||||
|
state = hass.states.get("event.backup_automatic_backup")
|
||||||
|
assert state.attributes[ATTR_EVENT_TYPE] == "failed"
|
||||||
|
assert state.attributes[ATTR_BACKUP_STAGE] is None
|
||||||
|
assert state.attributes[ATTR_FAILED_REASON] == "unknown_error"
|
Loading…
x
Reference in New Issue
Block a user