Skip to content

Commit e96e95c

Browse files
Add sensor platform to backup integration (#138663)
* add sensor platform to backup integration * adjust namings, remove system integration flag * add first simple test * apply review comments * fix test * add sensor tests * adjustements to use backup helper * remove obsolet async_get_manager from init * unsubscribe from events on entry unload * add configuration_url * fix doc string * fix sensor tests * mark async_unsubscribe as callback * set integration_type service * extend sensor test * set integration_type on correct integration :) * fix after online conflict resolution * add sensor update tests * simplify the sensor update tests * avoid io during tests * Add comment --------- Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 265a2ac commit e96e95c

File tree

13 files changed

+607
-4
lines changed

13 files changed

+607
-4
lines changed

homeassistant/components/backup/__init__.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""The Backup integration."""
22

3+
from homeassistant.config_entries import SOURCE_SYSTEM
4+
from homeassistant.const import Platform
35
from homeassistant.core import HomeAssistant, ServiceCall
4-
from homeassistant.helpers import config_validation as cv
6+
from homeassistant.helpers import config_validation as cv, discovery_flow
57
from homeassistant.helpers.backup import DATA_BACKUP
68
from homeassistant.helpers.hassio import is_hassio
79
from homeassistant.helpers.typing import ConfigType
@@ -18,10 +20,12 @@
1820
)
1921
from .config import BackupConfig, CreateBackupParametersDict
2022
from .const import DATA_MANAGER, DOMAIN
23+
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
2124
from .http import async_register_http_views
2225
from .manager import (
2326
BackupManager,
2427
BackupManagerError,
28+
BackupPlatformEvent,
2529
BackupPlatformProtocol,
2630
BackupReaderWriter,
2731
BackupReaderWriterError,
@@ -52,6 +56,7 @@
5256
"BackupConfig",
5357
"BackupManagerError",
5458
"BackupNotFound",
59+
"BackupPlatformEvent",
5560
"BackupPlatformProtocol",
5661
"BackupReaderWriter",
5762
"BackupReaderWriterError",
@@ -74,6 +79,8 @@
7479
"suggested_filename_from_name_date",
7580
]
7681

82+
PLATFORMS = [Platform.SENSOR]
83+
7784
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
7885

7986

@@ -128,4 +135,28 @@ async def async_handle_create_automatic_service(call: ServiceCall) -> None:
128135

129136
async_register_http_views(hass)
130137

138+
discovery_flow.async_create_flow(
139+
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
140+
)
141+
142+
return True
143+
144+
145+
async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
146+
"""Set up a config entry."""
147+
backup_manager: BackupManager = hass.data[DATA_MANAGER]
148+
coordinator = BackupDataUpdateCoordinator(hass, entry, backup_manager)
149+
await coordinator.async_config_entry_first_refresh()
150+
151+
entry.async_on_unload(coordinator.async_unsubscribe)
152+
153+
entry.runtime_data = coordinator
154+
155+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
156+
131157
return True
158+
159+
160+
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
161+
"""Unload a config entry."""
162+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Config flow for Home Assistant Backup integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
8+
9+
from .const import DOMAIN
10+
11+
12+
class BackupConfigFlow(ConfigFlow, domain=DOMAIN):
13+
"""Handle a config flow for Home Assistant Backup."""
14+
15+
VERSION = 1
16+
17+
async def async_step_system(
18+
self, user_input: dict[str, Any] | None = None
19+
) -> ConfigFlowResult:
20+
"""Handle the initial step."""
21+
return self.async_create_entry(title="Backup", data={})
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Coordinator for Home Assistant Backup integration."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
from datetime import datetime
8+
9+
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.core import HomeAssistant, callback
11+
from homeassistant.helpers.backup import (
12+
async_subscribe_events,
13+
async_subscribe_platform_events,
14+
)
15+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
16+
17+
from .const import DOMAIN, LOGGER
18+
from .manager import (
19+
BackupManager,
20+
BackupManagerState,
21+
BackupPlatformEvent,
22+
ManagerStateEvent,
23+
)
24+
25+
type BackupConfigEntry = ConfigEntry[BackupDataUpdateCoordinator]
26+
27+
28+
@dataclass
29+
class BackupCoordinatorData:
30+
"""Class to hold backup data."""
31+
32+
backup_manager_state: BackupManagerState
33+
last_successful_automatic_backup: datetime | None
34+
next_scheduled_automatic_backup: datetime | None
35+
36+
37+
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
38+
"""Class to retrieve backup status."""
39+
40+
config_entry: ConfigEntry
41+
42+
def __init__(
43+
self,
44+
hass: HomeAssistant,
45+
config_entry: ConfigEntry,
46+
backup_manager: BackupManager,
47+
) -> None:
48+
"""Initialize coordinator."""
49+
super().__init__(
50+
hass,
51+
LOGGER,
52+
config_entry=config_entry,
53+
name=DOMAIN,
54+
update_interval=None,
55+
)
56+
self.unsubscribe: list[Callable[[], None]] = [
57+
async_subscribe_events(hass, self._on_event),
58+
async_subscribe_platform_events(hass, self._on_event),
59+
]
60+
61+
self.backup_manager = backup_manager
62+
63+
@callback
64+
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
65+
"""Handle new event."""
66+
LOGGER.debug("Received backup event: %s", event)
67+
self.config_entry.async_create_task(self.hass, self.async_refresh())
68+
69+
async def _async_update_data(self) -> BackupCoordinatorData:
70+
"""Update backup manager data."""
71+
return BackupCoordinatorData(
72+
self.backup_manager.state,
73+
self.backup_manager.config.data.last_completed_automatic_backup,
74+
self.backup_manager.config.data.schedule.next_automatic_backup,
75+
)
76+
77+
@callback
78+
def async_unsubscribe(self) -> None:
79+
"""Unsubscribe from events."""
80+
for unsub in self.unsubscribe:
81+
unsub()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Base for backup entities."""
2+
3+
from __future__ import annotations
4+
5+
from homeassistant.const import __version__ as HA_VERSION
6+
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
7+
from homeassistant.helpers.entity import EntityDescription
8+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
9+
10+
from .const import DOMAIN
11+
from .coordinator import BackupDataUpdateCoordinator
12+
13+
14+
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
15+
"""Base entity for backup manager."""
16+
17+
_attr_has_entity_name = True
18+
19+
def __init__(
20+
self,
21+
coordinator: BackupDataUpdateCoordinator,
22+
entity_description: EntityDescription,
23+
) -> None:
24+
"""Initialize base entity."""
25+
super().__init__(coordinator)
26+
self.entity_description = entity_description
27+
self._attr_unique_id = entity_description.key
28+
self._attr_device_info = DeviceInfo(
29+
identifiers={(DOMAIN, "backup_manager")},
30+
manufacturer="Home Assistant",
31+
model="Home Assistant Backup",
32+
sw_version=HA_VERSION,
33+
name="Backup",
34+
entry_type=DeviceEntryType.SERVICE,
35+
configuration_url="homeassistant://config/backup",
36+
)

homeassistant/components/backup/manager.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ class RestoreBackupEvent(ManagerStateEvent):
229229
state: RestoreBackupState
230230

231231

232+
@dataclass(frozen=True, kw_only=True, slots=True)
233+
class BackupPlatformEvent:
234+
"""Backup platform class."""
235+
236+
domain: str
237+
238+
232239
@dataclass(frozen=True, kw_only=True, slots=True)
233240
class BlockedEvent(ManagerStateEvent):
234241
"""Backup manager blocked, Home Assistant is starting."""
@@ -355,6 +362,9 @@ def __init__(self, hass: HomeAssistant, reader_writer: BackupReaderWriter) -> No
355362
self._backup_event_subscriptions = hass.data[
356363
DATA_BACKUP
357364
].backup_event_subscriptions
365+
self._backup_platform_event_subscriptions = hass.data[
366+
DATA_BACKUP
367+
].backup_platform_event_subscriptions
358368

359369
async def async_setup(self) -> None:
360370
"""Set up the backup manager."""
@@ -465,6 +475,9 @@ async def _add_platform(
465475
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
466476
LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
467477
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
478+
event = BackupPlatformEvent(domain=integration_domain)
479+
for subscription in self._backup_platform_event_subscriptions:
480+
subscription(event)
468481

469482
async def async_pre_backup_actions(self) -> None:
470483
"""Perform pre backup actions."""

homeassistant/components/backup/manifest.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"codeowners": ["@home-assistant/core"],
66
"dependencies": ["http", "websocket_api"],
77
"documentation": "https://www.home-assistant.io/integrations/backup",
8-
"integration_type": "system",
8+
"integration_type": "service",
99
"iot_class": "calculated",
1010
"quality_scale": "internal",
11-
"requirements": ["cronsim==2.6", "securetar==2025.2.1"]
11+
"requirements": ["cronsim==2.6", "securetar==2025.2.1"],
12+
"single_config_entry": true
1213
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Sensor platform for Home Assistant Backup integration."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
from datetime import datetime
8+
9+
from homeassistant.components.sensor import (
10+
SensorDeviceClass,
11+
SensorEntity,
12+
SensorEntityDescription,
13+
)
14+
from homeassistant.core import HomeAssistant
15+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16+
17+
from .coordinator import BackupConfigEntry, BackupCoordinatorData
18+
from .entity import BackupManagerEntity
19+
from .manager import BackupManagerState
20+
21+
22+
@dataclass(kw_only=True, frozen=True)
23+
class BackupSensorEntityDescription(SensorEntityDescription):
24+
"""Description for Home Assistant Backup sensor entities."""
25+
26+
value_fn: Callable[[BackupCoordinatorData], str | datetime | None]
27+
28+
29+
BACKUP_MANAGER_DESCRIPTIONS = (
30+
BackupSensorEntityDescription(
31+
key="backup_manager_state",
32+
translation_key="backup_manager_state",
33+
device_class=SensorDeviceClass.ENUM,
34+
options=[state.value for state in BackupManagerState],
35+
value_fn=lambda data: data.backup_manager_state,
36+
),
37+
BackupSensorEntityDescription(
38+
key="next_scheduled_automatic_backup",
39+
translation_key="next_scheduled_automatic_backup",
40+
device_class=SensorDeviceClass.TIMESTAMP,
41+
value_fn=lambda data: data.next_scheduled_automatic_backup,
42+
),
43+
BackupSensorEntityDescription(
44+
key="last_successful_automatic_backup",
45+
translation_key="last_successful_automatic_backup",
46+
device_class=SensorDeviceClass.TIMESTAMP,
47+
value_fn=lambda data: data.last_successful_automatic_backup,
48+
),
49+
)
50+
51+
52+
async def async_setup_entry(
53+
hass: HomeAssistant,
54+
config_entry: BackupConfigEntry,
55+
async_add_entities: AddConfigEntryEntitiesCallback,
56+
) -> None:
57+
"""Sensor set up for backup config entry."""
58+
59+
coordinator = config_entry.runtime_data
60+
61+
async_add_entities(
62+
BackupManagerSensor(coordinator, description)
63+
for description in BACKUP_MANAGER_DESCRIPTIONS
64+
)
65+
66+
67+
class BackupManagerSensor(BackupManagerEntity, SensorEntity):
68+
"""Sensor to track backup manager state."""
69+
70+
entity_description: BackupSensorEntityDescription
71+
72+
@property
73+
def native_value(self) -> str | datetime | None:
74+
"""Return native value of entity."""
75+
return self.entity_description.value_fn(self.coordinator.data)

homeassistant/components/backup/strings.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,24 @@
2222
"name": "Create automatic backup",
2323
"description": "Creates a new backup with automatic backup settings."
2424
}
25+
},
26+
"entity": {
27+
"sensor": {
28+
"backup_manager_state": {
29+
"name": "Backup Manager State",
30+
"state": {
31+
"idle": "Idle",
32+
"create_backup": "Creating a backup",
33+
"receive_backup": "Receiving a backup",
34+
"restore_backup": "Restoring a backup"
35+
}
36+
},
37+
"next_scheduled_automatic_backup": {
38+
"name": "Next scheduled automatic backup"
39+
},
40+
"last_successful_automatic_backup": {
41+
"name": "Last successful automatic backup"
42+
}
43+
}
2544
}
2645
}

homeassistant/generated/integrations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,13 @@
611611
"config_flow": true,
612612
"iot_class": "local_push"
613613
},
614+
"backup": {
615+
"name": "Backup",
616+
"integration_type": "service",
617+
"config_flow": false,
618+
"iot_class": "calculated",
619+
"single_config_entry": true
620+
},
614621
"baf": {
615622
"name": "Big Ass Fans",
616623
"integration_type": "hub",

0 commit comments

Comments
 (0)