mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
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 <marhje52@gmail.com>
This commit is contained in:
parent
265a2ace90
commit
e96e95c32d
@ -1,7 +1,9 @@
|
|||||||
"""The Backup integration."""
|
"""The Backup integration."""
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_SYSTEM
|
||||||
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||||
from homeassistant.helpers.backup import DATA_BACKUP
|
from homeassistant.helpers.backup import DATA_BACKUP
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -18,10 +20,12 @@ from .agent import (
|
|||||||
)
|
)
|
||||||
from .config import BackupConfig, CreateBackupParametersDict
|
from .config import BackupConfig, CreateBackupParametersDict
|
||||||
from .const import DATA_MANAGER, DOMAIN
|
from .const import DATA_MANAGER, DOMAIN
|
||||||
|
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
|
||||||
from .http import async_register_http_views
|
from .http import async_register_http_views
|
||||||
from .manager import (
|
from .manager import (
|
||||||
BackupManager,
|
BackupManager,
|
||||||
BackupManagerError,
|
BackupManagerError,
|
||||||
|
BackupPlatformEvent,
|
||||||
BackupPlatformProtocol,
|
BackupPlatformProtocol,
|
||||||
BackupReaderWriter,
|
BackupReaderWriter,
|
||||||
BackupReaderWriterError,
|
BackupReaderWriterError,
|
||||||
@ -52,6 +56,7 @@ __all__ = [
|
|||||||
"BackupConfig",
|
"BackupConfig",
|
||||||
"BackupManagerError",
|
"BackupManagerError",
|
||||||
"BackupNotFound",
|
"BackupNotFound",
|
||||||
|
"BackupPlatformEvent",
|
||||||
"BackupPlatformProtocol",
|
"BackupPlatformProtocol",
|
||||||
"BackupReaderWriter",
|
"BackupReaderWriter",
|
||||||
"BackupReaderWriterError",
|
"BackupReaderWriterError",
|
||||||
@ -74,6 +79,8 @@ __all__ = [
|
|||||||
"suggested_filename_from_name_date",
|
"suggested_filename_from_name_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
@ -128,4 +135,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async_register_http_views(hass)
|
async_register_http_views(hass)
|
||||||
|
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
backup_manager: BackupManager = hass.data[DATA_MANAGER]
|
||||||
|
coordinator = BackupDataUpdateCoordinator(hass, entry, backup_manager)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_unsubscribe)
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
21
homeassistant/components/backup/config_flow.py
Normal file
21
homeassistant/components/backup/config_flow.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"""Config flow for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class BackupConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Home Assistant Backup."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_system(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
return self.async_create_entry(title="Backup", data={})
|
81
homeassistant/components/backup/coordinator.py
Normal file
81
homeassistant/components/backup/coordinator.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""Coordinator for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.backup import (
|
||||||
|
async_subscribe_events,
|
||||||
|
async_subscribe_platform_events,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .manager import (
|
||||||
|
BackupManager,
|
||||||
|
BackupManagerState,
|
||||||
|
BackupPlatformEvent,
|
||||||
|
ManagerStateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackupConfigEntry = ConfigEntry[BackupDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BackupCoordinatorData:
|
||||||
|
"""Class to hold backup data."""
|
||||||
|
|
||||||
|
backup_manager_state: BackupManagerState
|
||||||
|
last_successful_automatic_backup: datetime | None
|
||||||
|
next_scheduled_automatic_backup: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
||||||
|
"""Class to retrieve backup status."""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
backup_manager: BackupManager,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=None,
|
||||||
|
)
|
||||||
|
self.unsubscribe: list[Callable[[], None]] = [
|
||||||
|
async_subscribe_events(hass, self._on_event),
|
||||||
|
async_subscribe_platform_events(hass, self._on_event),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.backup_manager = backup_manager
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
|
||||||
|
"""Handle new event."""
|
||||||
|
LOGGER.debug("Received backup event: %s", event)
|
||||||
|
self.config_entry.async_create_task(self.hass, self.async_refresh())
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> BackupCoordinatorData:
|
||||||
|
"""Update backup manager data."""
|
||||||
|
return BackupCoordinatorData(
|
||||||
|
self.backup_manager.state,
|
||||||
|
self.backup_manager.config.data.last_completed_automatic_backup,
|
||||||
|
self.backup_manager.config.data.schedule.next_automatic_backup,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_unsubscribe(self) -> None:
|
||||||
|
"""Unsubscribe from events."""
|
||||||
|
for unsub in self.unsubscribe:
|
||||||
|
unsub()
|
36
homeassistant/components/backup/entity.py
Normal file
36
homeassistant/components/backup/entity.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Base for backup entities."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import __version__ as HA_VERSION
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import BackupDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
||||||
|
"""Base entity for backup manager."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BackupDataUpdateCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize base entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = entity_description.key
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, "backup_manager")},
|
||||||
|
manufacturer="Home Assistant",
|
||||||
|
model="Home Assistant Backup",
|
||||||
|
sw_version=HA_VERSION,
|
||||||
|
name="Backup",
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
configuration_url="homeassistant://config/backup",
|
||||||
|
)
|
@ -229,6 +229,13 @@ class RestoreBackupEvent(ManagerStateEvent):
|
|||||||
state: RestoreBackupState
|
state: RestoreBackupState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||||
|
class BackupPlatformEvent:
|
||||||
|
"""Backup platform class."""
|
||||||
|
|
||||||
|
domain: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||||
class BlockedEvent(ManagerStateEvent):
|
class BlockedEvent(ManagerStateEvent):
|
||||||
"""Backup manager blocked, Home Assistant is starting."""
|
"""Backup manager blocked, Home Assistant is starting."""
|
||||||
@ -355,6 +362,9 @@ class BackupManager:
|
|||||||
self._backup_event_subscriptions = hass.data[
|
self._backup_event_subscriptions = hass.data[
|
||||||
DATA_BACKUP
|
DATA_BACKUP
|
||||||
].backup_event_subscriptions
|
].backup_event_subscriptions
|
||||||
|
self._backup_platform_event_subscriptions = hass.data[
|
||||||
|
DATA_BACKUP
|
||||||
|
].backup_platform_event_subscriptions
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Set up the backup manager."""
|
"""Set up the backup manager."""
|
||||||
@ -465,6 +475,9 @@ class BackupManager:
|
|||||||
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
|
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
|
||||||
LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
|
LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
|
||||||
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
|
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
|
||||||
|
event = BackupPlatformEvent(domain=integration_domain)
|
||||||
|
for subscription in self._backup_platform_event_subscriptions:
|
||||||
|
subscription(event)
|
||||||
|
|
||||||
async def async_pre_backup_actions(self) -> None:
|
async def async_pre_backup_actions(self) -> None:
|
||||||
"""Perform pre backup actions."""
|
"""Perform pre backup actions."""
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["http", "websocket_api"],
|
"dependencies": ["http", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/backup",
|
"documentation": "https://www.home-assistant.io/integrations/backup",
|
||||||
"integration_type": "system",
|
"integration_type": "service",
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["cronsim==2.6", "securetar==2025.2.1"]
|
"requirements": ["cronsim==2.6", "securetar==2025.2.1"],
|
||||||
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
75
homeassistant/components/backup/sensor.py
Normal file
75
homeassistant/components/backup/sensor.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""Sensor platform for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import BackupConfigEntry, BackupCoordinatorData
|
||||||
|
from .entity import BackupManagerEntity
|
||||||
|
from .manager import BackupManagerState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class BackupSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Description for Home Assistant Backup sensor entities."""
|
||||||
|
|
||||||
|
value_fn: Callable[[BackupCoordinatorData], str | datetime | None]
|
||||||
|
|
||||||
|
|
||||||
|
BACKUP_MANAGER_DESCRIPTIONS = (
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="backup_manager_state",
|
||||||
|
translation_key="backup_manager_state",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[state.value for state in BackupManagerState],
|
||||||
|
value_fn=lambda data: data.backup_manager_state,
|
||||||
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="next_scheduled_automatic_backup",
|
||||||
|
translation_key="next_scheduled_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.next_scheduled_automatic_backup,
|
||||||
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="last_successful_automatic_backup",
|
||||||
|
translation_key="last_successful_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.last_successful_automatic_backup,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BackupConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Sensor set up for backup config entry."""
|
||||||
|
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
BackupManagerSensor(coordinator, description)
|
||||||
|
for description in BACKUP_MANAGER_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManagerSensor(BackupManagerEntity, SensorEntity):
|
||||||
|
"""Sensor to track backup manager state."""
|
||||||
|
|
||||||
|
entity_description: BackupSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> str | datetime | None:
|
||||||
|
"""Return native value of entity."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
@ -22,5 +22,24 @@
|
|||||||
"name": "Create automatic backup",
|
"name": "Create automatic backup",
|
||||||
"description": "Creates a new backup with automatic backup settings."
|
"description": "Creates a new backup with automatic backup settings."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"backup_manager_state": {
|
||||||
|
"name": "Backup Manager State",
|
||||||
|
"state": {
|
||||||
|
"idle": "Idle",
|
||||||
|
"create_backup": "Creating a backup",
|
||||||
|
"receive_backup": "Receiving a backup",
|
||||||
|
"restore_backup": "Restoring a backup"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"next_scheduled_automatic_backup": {
|
||||||
|
"name": "Next scheduled automatic backup"
|
||||||
|
},
|
||||||
|
"last_successful_automatic_backup": {
|
||||||
|
"name": "Last successful automatic backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,6 +611,13 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
},
|
},
|
||||||
|
"backup": {
|
||||||
|
"name": "Backup",
|
||||||
|
"integration_type": "service",
|
||||||
|
"config_flow": false,
|
||||||
|
"iot_class": "calculated",
|
||||||
|
"single_config_entry": true
|
||||||
|
},
|
||||||
"baf": {
|
"baf": {
|
||||||
"name": "Big Ass Fans",
|
"name": "Big Ass Fans",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
@ -12,7 +12,11 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.backup import BackupManager, ManagerStateEvent
|
from homeassistant.components.backup import (
|
||||||
|
BackupManager,
|
||||||
|
BackupPlatformEvent,
|
||||||
|
ManagerStateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
DATA_BACKUP: HassKey[BackupData] = HassKey("backup_data")
|
DATA_BACKUP: HassKey[BackupData] = HassKey("backup_data")
|
||||||
DATA_MANAGER: HassKey[BackupManager] = HassKey("backup")
|
DATA_MANAGER: HassKey[BackupManager] = HassKey("backup")
|
||||||
@ -25,6 +29,9 @@ class BackupData:
|
|||||||
backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = field(
|
backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = field(
|
||||||
default_factory=list
|
default_factory=list
|
||||||
)
|
)
|
||||||
|
backup_platform_event_subscriptions: list[Callable[[BackupPlatformEvent], None]] = (
|
||||||
|
field(default_factory=list)
|
||||||
|
)
|
||||||
manager_ready: asyncio.Future[None] = field(default_factory=asyncio.Future)
|
manager_ready: asyncio.Future[None] = field(default_factory=asyncio.Future)
|
||||||
|
|
||||||
|
|
||||||
@ -68,3 +75,20 @@ def async_subscribe_events(
|
|||||||
|
|
||||||
backup_event_subscriptions.append(on_event)
|
backup_event_subscriptions.append(on_event)
|
||||||
return remove_subscription
|
return remove_subscription
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_subscribe_platform_events(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
on_event: Callable[[BackupPlatformEvent], None],
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Subscribe to backup platform events."""
|
||||||
|
backup_platform_event_subscriptions = hass.data[
|
||||||
|
DATA_BACKUP
|
||||||
|
].backup_platform_event_subscriptions
|
||||||
|
|
||||||
|
def remove_subscription() -> None:
|
||||||
|
backup_platform_event_subscriptions.remove(on_event)
|
||||||
|
|
||||||
|
backup_platform_event_subscriptions.append(on_event)
|
||||||
|
return remove_subscription
|
||||||
|
160
tests/components/backup/snapshots/test_sensors.ambr
Normal file
160
tests/components/backup/snapshots/test_sensors.ambr
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_sensors[sensor.backup_backup_manager_state-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'idle',
|
||||||
|
'create_backup',
|
||||||
|
'blocked',
|
||||||
|
'receive_backup',
|
||||||
|
'restore_backup',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.backup_backup_manager_state',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Backup Manager State',
|
||||||
|
'platform': 'backup',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'backup_manager_state',
|
||||||
|
'unique_id': 'backup_manager_state',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.backup_backup_manager_state-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'enum',
|
||||||
|
'friendly_name': 'Backup Backup Manager State',
|
||||||
|
'options': list([
|
||||||
|
'idle',
|
||||||
|
'create_backup',
|
||||||
|
'blocked',
|
||||||
|
'receive_backup',
|
||||||
|
'restore_backup',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.backup_backup_manager_state',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'idle',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.backup_last_successful_automatic_backup-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.backup_last_successful_automatic_backup',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Last successful automatic backup',
|
||||||
|
'platform': 'backup',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'last_successful_automatic_backup',
|
||||||
|
'unique_id': 'last_successful_automatic_backup',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.backup_last_successful_automatic_backup-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'timestamp',
|
||||||
|
'friendly_name': 'Backup Last successful automatic backup',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.backup_last_successful_automatic_backup',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.backup_next_scheduled_automatic_backup-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.backup_next_scheduled_automatic_backup',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Next scheduled automatic backup',
|
||||||
|
'platform': 'backup',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'next_scheduled_automatic_backup',
|
||||||
|
'unique_id': 'next_scheduled_automatic_backup',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.backup_next_scheduled_automatic_backup-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'timestamp',
|
||||||
|
'friendly_name': 'Backup Next scheduled automatic backup',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.backup_next_scheduled_automatic_backup',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
@ -6,11 +6,13 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
|
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
|
|
||||||
from .common import setup_backup_integration
|
from .common import setup_backup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
@ -141,3 +143,17 @@ async def test_create_automatic_service(
|
|||||||
)
|
)
|
||||||
|
|
||||||
generate_backup.assert_called_once_with(**expected_kwargs)
|
generate_backup.assert_called_once_with(**expected_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup backup config entry."""
|
||||||
|
await setup_backup_integration(hass, with_hassio=False)
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, source=SOURCE_SYSTEM)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", return_value=[]):
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
119
tests/components/backup/test_sensors.py
Normal file
119
tests/components/backup/test_sensors.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"""Tests for the sensors of the Backup integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.backup import store
|
||||||
|
from homeassistant.components.backup.const import DOMAIN
|
||||||
|
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 async_fire_time_changed, snapshot_platform
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_backup_generation")
|
||||||
|
async def test_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup of backup sensors."""
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
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)
|
||||||
|
|
||||||
|
# start backup and check sensor states again
|
||||||
|
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("sensor.backup_backup_manager_state")
|
||||||
|
assert state.state == "create_backup"
|
||||||
|
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
state = hass.states.get("sensor.backup_backup_manager_state")
|
||||||
|
assert state.state == "idle"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_updates(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
create_backup: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test update of backup sensors."""
|
||||||
|
# Ensure created backup is already protected,
|
||||||
|
# to avoid manager creating a new EncryptedBackupStreamer
|
||||||
|
# instead of using the already mocked stream writer.
|
||||||
|
created_backup: MagicMock = create_backup.return_value[1].result().backup
|
||||||
|
created_backup.protected = True
|
||||||
|
|
||||||
|
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||||
|
freezer.move_to("2024-11-12T12:00:00+01:00")
|
||||||
|
storage_data = {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"automatic_backups_configured": True,
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["test.remote"],
|
||||||
|
"include_addons": [],
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": True,
|
||||||
|
"include_folders": [],
|
||||||
|
"name": "test-name",
|
||||||
|
"password": "test-password",
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": "2024-11-11T04:45:00+01:00",
|
||||||
|
"last_completed_automatic_backup": "2024-11-11T04:45:00+01:00",
|
||||||
|
"schedule": {
|
||||||
|
"days": [],
|
||||||
|
"recurrence": "daily",
|
||||||
|
"state": "never",
|
||||||
|
"time": "06:00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hass_storage[DOMAIN] = {
|
||||||
|
"data": storage_data,
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": store.STORAGE_VERSION,
|
||||||
|
"minor_version": store.STORAGE_VERSION_MINOR,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("homeassistant.components.backup.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
await setup_backup_integration(
|
||||||
|
hass, with_hassio=False, remote_agents=["test.remote"]
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.backup_last_successful_automatic_backup")
|
||||||
|
assert state.state == "2024-11-11T03:45:00+00:00"
|
||||||
|
state = hass.states.get("sensor.backup_next_scheduled_automatic_backup")
|
||||||
|
assert state.state == "2024-11-13T05:00:00+00:00"
|
||||||
|
|
||||||
|
freezer.move_to("2024-11-13T12:00:00+01:00")
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.backup_last_successful_automatic_backup")
|
||||||
|
assert state.state == "2024-11-13T11:00:00+00:00"
|
||||||
|
state = hass.states.get("sensor.backup_next_scheduled_automatic_backup")
|
||||||
|
assert state.state == "2024-11-14T05:00:00+00:00"
|
Loading…
x
Reference in New Issue
Block a user