mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
2025.2.5 (#139012)
This commit is contained in:
commit
cc792403ab
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airgradient==0.9.1"],
|
"requirements": ["airgradient==0.9.2"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ from .agent import (
|
|||||||
BackupAgentPlatformProtocol,
|
BackupAgentPlatformProtocol,
|
||||||
LocalBackupAgent,
|
LocalBackupAgent,
|
||||||
)
|
)
|
||||||
|
from .config import BackupConfig, CreateBackupParametersDict
|
||||||
from .const import DATA_MANAGER, DOMAIN
|
from .const import DATA_MANAGER, DOMAIN
|
||||||
from .http import async_register_http_views
|
from .http import async_register_http_views
|
||||||
from .manager import (
|
from .manager import (
|
||||||
@ -47,12 +48,14 @@ __all__ = [
|
|||||||
"BackupAgent",
|
"BackupAgent",
|
||||||
"BackupAgentError",
|
"BackupAgentError",
|
||||||
"BackupAgentPlatformProtocol",
|
"BackupAgentPlatformProtocol",
|
||||||
|
"BackupConfig",
|
||||||
"BackupManagerError",
|
"BackupManagerError",
|
||||||
"BackupNotFound",
|
"BackupNotFound",
|
||||||
"BackupPlatformProtocol",
|
"BackupPlatformProtocol",
|
||||||
"BackupReaderWriter",
|
"BackupReaderWriter",
|
||||||
"BackupReaderWriterError",
|
"BackupReaderWriterError",
|
||||||
"CreateBackupEvent",
|
"CreateBackupEvent",
|
||||||
|
"CreateBackupParametersDict",
|
||||||
"CreateBackupStage",
|
"CreateBackupStage",
|
||||||
"CreateBackupState",
|
"CreateBackupState",
|
||||||
"Folder",
|
"Folder",
|
||||||
|
@ -154,7 +154,8 @@ class BackupConfig:
|
|||||||
self.data.retention.apply(self._manager)
|
self.data.retention.apply(self._manager)
|
||||||
self.data.schedule.apply(self._manager)
|
self.data.schedule.apply(self._manager)
|
||||||
|
|
||||||
async def update(
|
@callback
|
||||||
|
def update(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED,
|
agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED,
|
||||||
|
@ -43,7 +43,11 @@ from .agent import (
|
|||||||
BackupAgentPlatformProtocol,
|
BackupAgentPlatformProtocol,
|
||||||
LocalBackupAgent,
|
LocalBackupAgent,
|
||||||
)
|
)
|
||||||
from .config import BackupConfig, delete_backups_exceeding_configured_count
|
from .config import (
|
||||||
|
BackupConfig,
|
||||||
|
CreateBackupParametersDict,
|
||||||
|
delete_backups_exceeding_configured_count,
|
||||||
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
BUF_SIZE,
|
BUF_SIZE,
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
@ -282,6 +286,10 @@ class BackupReaderWriter(abc.ABC):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Get restore events after core restart."""
|
"""Get restore events after core restart."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||||
|
"""Validate backup config."""
|
||||||
|
|
||||||
|
|
||||||
class IncorrectPasswordError(BackupReaderWriterError):
|
class IncorrectPasswordError(BackupReaderWriterError):
|
||||||
"""Raised when the password is incorrect."""
|
"""Raised when the password is incorrect."""
|
||||||
@ -333,6 +341,7 @@ class BackupManager:
|
|||||||
self.config.load(stored["config"])
|
self.config.load(stored["config"])
|
||||||
self.known_backups.load(stored["backups"])
|
self.known_backups.load(stored["backups"])
|
||||||
|
|
||||||
|
await self._reader_writer.async_validate_config(config=self.config)
|
||||||
await self._reader_writer.async_resume_restore_progress_after_restart(
|
await self._reader_writer.async_resume_restore_progress_after_restart(
|
||||||
on_progress=self.async_on_backup_event
|
on_progress=self.async_on_backup_event
|
||||||
)
|
)
|
||||||
@ -1832,6 +1841,44 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
|||||||
)
|
)
|
||||||
on_progress(IdleEvent())
|
on_progress(IdleEvent())
|
||||||
|
|
||||||
|
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||||
|
"""Validate backup config.
|
||||||
|
|
||||||
|
Update automatic backup settings to not include addons or folders and remove
|
||||||
|
hassio agents in case a backup created by supervisor was restored.
|
||||||
|
"""
|
||||||
|
create_backup = config.data.create_backup
|
||||||
|
if (
|
||||||
|
not create_backup.include_addons
|
||||||
|
and not create_backup.include_all_addons
|
||||||
|
and not create_backup.include_folders
|
||||||
|
and not any(a_id.startswith("hassio.") for a_id in create_backup.agent_ids)
|
||||||
|
):
|
||||||
|
LOGGER.debug("Backup settings don't need to be adjusted")
|
||||||
|
return
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Adjusting backup settings to not include addons, folders or supervisor locations"
|
||||||
|
)
|
||||||
|
automatic_agents = [
|
||||||
|
agent_id
|
||||||
|
for agent_id in create_backup.agent_ids
|
||||||
|
if not agent_id.startswith("hassio.")
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
self._local_agent_id not in automatic_agents
|
||||||
|
and "hassio.local" in create_backup.agent_ids
|
||||||
|
):
|
||||||
|
automatic_agents = [self._local_agent_id, *automatic_agents]
|
||||||
|
config.update(
|
||||||
|
create_backup=CreateBackupParametersDict(
|
||||||
|
agent_ids=automatic_agents,
|
||||||
|
include_addons=None,
|
||||||
|
include_all_addons=False,
|
||||||
|
include_folders=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_backup_id(date: str, name: str) -> str:
|
def _generate_backup_id(date: str, name: str) -> str:
|
||||||
"""Generate a backup ID."""
|
"""Generate a backup ID."""
|
||||||
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|||||||
STORE_DELAY_SAVE = 30
|
STORE_DELAY_SAVE = 30
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_VERSION_MINOR = 3
|
STORAGE_VERSION_MINOR = 4
|
||||||
|
|
||||||
|
|
||||||
class StoredBackupData(TypedDict):
|
class StoredBackupData(TypedDict):
|
||||||
@ -60,6 +60,13 @@ class _BackupStore(Store[StoredBackupData]):
|
|||||||
else:
|
else:
|
||||||
data["config"]["schedule"]["days"] = [state]
|
data["config"]["schedule"]["days"] = [state]
|
||||||
data["config"]["schedule"]["recurrence"] = "custom_days"
|
data["config"]["schedule"]["recurrence"] = "custom_days"
|
||||||
|
if old_minor_version < 4:
|
||||||
|
# Workaround for a bug in frontend which incorrectly set days to 0
|
||||||
|
# instead of to None for unlimited retention.
|
||||||
|
if data["config"]["retention"]["copies"] == 0:
|
||||||
|
data["config"]["retention"]["copies"] = None
|
||||||
|
if data["config"]["retention"]["days"] == 0:
|
||||||
|
data["config"]["retention"]["days"] = None
|
||||||
|
|
||||||
# Note: We allow reading data with major version 2.
|
# Note: We allow reading data with major version 2.
|
||||||
# Reject if major version is higher than 2.
|
# Reject if major version is higher than 2.
|
||||||
|
@ -104,12 +104,15 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
|||||||
bool, homeassistant.get("exclude_database", False)
|
bool, homeassistant.get("exclude_database", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
||||||
|
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
||||||
|
|
||||||
return AgentBackup(
|
return AgentBackup(
|
||||||
addons=addons,
|
addons=addons,
|
||||||
backup_id=cast(str, data["slug"]),
|
backup_id=cast(str, data["slug"]),
|
||||||
database_included=database_included,
|
database_included=database_included,
|
||||||
date=cast(str, data["date"]),
|
date=cast(str, date),
|
||||||
extra_metadata=cast(dict[str, bool | str], data.get("extra", {})),
|
extra_metadata=extra_metadata,
|
||||||
folders=folders,
|
folders=folders,
|
||||||
homeassistant_included=homeassistant_included,
|
homeassistant_included=homeassistant_included,
|
||||||
homeassistant_version=homeassistant_version,
|
homeassistant_version=homeassistant_version,
|
||||||
|
@ -346,6 +346,7 @@ async def handle_config_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
@ -368,8 +369,10 @@ async def handle_config_info(
|
|||||||
),
|
),
|
||||||
vol.Optional("retention"): vol.Schema(
|
vol.Optional("retention"): vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional("copies"): vol.Any(int, None),
|
# Note: We can't use cv.positive_int because it allows 0 even
|
||||||
vol.Optional("days"): vol.Any(int, None),
|
# though 0 is not positive.
|
||||||
|
vol.Optional("copies"): vol.Any(vol.All(int, vol.Range(min=1)), None),
|
||||||
|
vol.Optional("days"): vol.Any(vol.All(int, vol.Range(min=1)), None),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
vol.Optional("schedule"): vol.Schema(
|
vol.Optional("schedule"): vol.Schema(
|
||||||
@ -385,8 +388,7 @@ async def handle_config_info(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
def handle_config_update(
|
||||||
async def handle_config_update(
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
@ -396,7 +398,7 @@ async def handle_config_update(
|
|||||||
changes = dict(msg)
|
changes = dict(msg)
|
||||||
changes.pop("id")
|
changes.pop("id")
|
||||||
changes.pop("type")
|
changes.pop("type")
|
||||||
await manager.config.update(**changes)
|
manager.config.update(**changes)
|
||||||
connection.send_result(msg["id"])
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,11 @@ from typing import Any
|
|||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from hass_nabucasa import Cloud, CloudError
|
from hass_nabucasa import Cloud, CloudError
|
||||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||||
from hass_nabucasa.cloud_api import async_files_delete_file, async_files_list
|
from hass_nabucasa.cloud_api import (
|
||||||
|
FilesHandlerListEntry,
|
||||||
|
async_files_delete_file,
|
||||||
|
async_files_list,
|
||||||
|
)
|
||||||
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
|
from hass_nabucasa.files import FilesError, StorageType, calculate_b64md5
|
||||||
|
|
||||||
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
||||||
@ -76,11 +80,6 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@callback
|
|
||||||
def _get_backup_filename(self) -> str:
|
|
||||||
"""Return the backup filename."""
|
|
||||||
return f"{self._cloud.client.prefs.instance_id}.tar"
|
|
||||||
|
|
||||||
async def async_download_backup(
|
async def async_download_backup(
|
||||||
self,
|
self,
|
||||||
backup_id: str,
|
backup_id: str,
|
||||||
@ -91,13 +90,13 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||||
:return: An async iterator that yields bytes.
|
:return: An async iterator that yields bytes.
|
||||||
"""
|
"""
|
||||||
if not await self.async_get_backup(backup_id):
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
raise BackupAgentError("Backup not found")
|
raise BackupAgentError("Backup not found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await self._cloud.files.download(
|
content = await self._cloud.files.download(
|
||||||
storage_type=StorageType.BACKUP,
|
storage_type=StorageType.BACKUP,
|
||||||
filename=self._get_backup_filename(),
|
filename=backup["Key"],
|
||||||
)
|
)
|
||||||
except CloudError as err:
|
except CloudError as err:
|
||||||
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
raise BackupAgentError(f"Failed to download backup: {err}") from err
|
||||||
@ -124,7 +123,7 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
base64md5hash = await calculate_b64md5(open_stream, size)
|
base64md5hash = await calculate_b64md5(open_stream, size)
|
||||||
except FilesError as err:
|
except FilesError as err:
|
||||||
raise BackupAgentError(err) from err
|
raise BackupAgentError(err) from err
|
||||||
filename = self._get_backup_filename()
|
filename = f"{self._cloud.client.prefs.instance_id}.tar"
|
||||||
metadata = backup.as_dict()
|
metadata = backup.as_dict()
|
||||||
|
|
||||||
tries = 1
|
tries = 1
|
||||||
@ -172,29 +171,34 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
|
|
||||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||||
"""
|
"""
|
||||||
if not await self.async_get_backup(backup_id):
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await async_files_delete_file(
|
await async_files_delete_file(
|
||||||
self._cloud,
|
self._cloud,
|
||||||
storage_type=StorageType.BACKUP,
|
storage_type=StorageType.BACKUP,
|
||||||
filename=self._get_backup_filename(),
|
filename=backup["Key"],
|
||||||
)
|
)
|
||||||
except (ClientError, CloudError) as err:
|
except (ClientError, CloudError) as err:
|
||||||
raise BackupAgentError("Failed to delete backup") from err
|
raise BackupAgentError("Failed to delete backup") from err
|
||||||
|
|
||||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||||
|
"""List backups."""
|
||||||
|
backups = await self._async_list_backups()
|
||||||
|
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
||||||
|
|
||||||
|
async def _async_list_backups(self) -> list[FilesHandlerListEntry]:
|
||||||
"""List backups."""
|
"""List backups."""
|
||||||
try:
|
try:
|
||||||
backups = await async_files_list(
|
backups = await async_files_list(
|
||||||
self._cloud, storage_type=StorageType.BACKUP
|
self._cloud, storage_type=StorageType.BACKUP
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Cloud backups: %s", backups)
|
|
||||||
except (ClientError, CloudError) as err:
|
except (ClientError, CloudError) as err:
|
||||||
raise BackupAgentError("Failed to list backups") from err
|
raise BackupAgentError("Failed to list backups") from err
|
||||||
|
|
||||||
return [AgentBackup.from_dict(backup["Metadata"]) for backup in backups]
|
_LOGGER.debug("Cloud backups: %s", backups)
|
||||||
|
return backups
|
||||||
|
|
||||||
async def async_get_backup(
|
async def async_get_backup(
|
||||||
self,
|
self,
|
||||||
@ -202,10 +206,19 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> AgentBackup | None:
|
) -> AgentBackup | None:
|
||||||
"""Return a backup."""
|
"""Return a backup."""
|
||||||
backups = await self.async_list_backups()
|
if not (backup := await self._async_get_backup(backup_id)):
|
||||||
|
return None
|
||||||
|
return AgentBackup.from_dict(backup["Metadata"])
|
||||||
|
|
||||||
|
async def _async_get_backup(
|
||||||
|
self,
|
||||||
|
backup_id: str,
|
||||||
|
) -> FilesHandlerListEntry | None:
|
||||||
|
"""Return a backup."""
|
||||||
|
backups = await self._async_list_backups()
|
||||||
|
|
||||||
for backup in backups:
|
for backup in backups:
|
||||||
if backup.backup_id == backup_id:
|
if backup["Metadata"]["backup_id"] == backup_id:
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.1.0"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==12.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,8 @@ async def async_setup_entry(
|
|||||||
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||||
"""The thermostat class for FRITZ!SmartHome thermostats."""
|
"""The thermostat class for FRITZ!SmartHome thermostats."""
|
||||||
|
|
||||||
|
_attr_max_temp = MAX_TEMPERATURE
|
||||||
|
_attr_min_temp = MIN_TEMPERATURE
|
||||||
_attr_precision = PRECISION_HALVES
|
_attr_precision = PRECISION_HALVES
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_translation_key = "thermostat"
|
_attr_translation_key = "thermostat"
|
||||||
@ -135,11 +137,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
|
||||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
|
||||||
await self.async_set_hvac_mode(hvac_mode)
|
await self.async_set_hvac_mode(hvac_mode)
|
||||||
elif target_temp is not None:
|
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||||
|
if target_temp == OFF_API_TEMPERATURE:
|
||||||
|
target_temp = OFF_REPORT_SET_TEMPERATURE
|
||||||
|
elif target_temp == ON_API_TEMPERATURE:
|
||||||
|
target_temp = ON_REPORT_SET_TEMPERATURE
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
self.data.set_target_temperature, target_temp, True
|
self.data.set_target_temperature, target_temp, True
|
||||||
)
|
)
|
||||||
@ -169,12 +173,12 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="change_hvac_while_active_mode",
|
translation_key="change_hvac_while_active_mode",
|
||||||
)
|
)
|
||||||
if self.hvac_mode == hvac_mode:
|
if self.hvac_mode is hvac_mode:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode is HVACMode.OFF:
|
||||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||||
else:
|
else:
|
||||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||||
@ -208,16 +212,6 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
elif preset_mode == PRESET_ECO:
|
elif preset_mode == PRESET_ECO:
|
||||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||||
|
|
||||||
@property
|
|
||||||
def min_temp(self) -> int:
|
|
||||||
"""Return the minimum temperature."""
|
|
||||||
return MIN_TEMPERATURE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_temp(self) -> int:
|
|
||||||
"""Return the maximum temperature."""
|
|
||||||
return MAX_TEMPERATURE
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyfritzhome"],
|
"loggers": ["pyfritzhome"],
|
||||||
"requirements": ["pyfritzhome==0.6.14"],
|
"requirements": ["pyfritzhome==0.6.15"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||||
|
@ -21,5 +21,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250214.0"]
|
"requirements": ["home-assistant-frontend==20250221.0"]
|
||||||
}
|
}
|
||||||
|
@ -27,11 +27,13 @@ from homeassistant.components.backup import (
|
|||||||
AddonInfo,
|
AddonInfo,
|
||||||
AgentBackup,
|
AgentBackup,
|
||||||
BackupAgent,
|
BackupAgent,
|
||||||
|
BackupConfig,
|
||||||
BackupManagerError,
|
BackupManagerError,
|
||||||
BackupNotFound,
|
BackupNotFound,
|
||||||
BackupReaderWriter,
|
BackupReaderWriter,
|
||||||
BackupReaderWriterError,
|
BackupReaderWriterError,
|
||||||
CreateBackupEvent,
|
CreateBackupEvent,
|
||||||
|
CreateBackupParametersDict,
|
||||||
CreateBackupStage,
|
CreateBackupStage,
|
||||||
CreateBackupState,
|
CreateBackupState,
|
||||||
Folder,
|
Folder,
|
||||||
@ -633,6 +635,27 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
_LOGGER.debug("Could not get restore job %s: %s", restore_job_id, err)
|
_LOGGER.debug("Could not get restore job %s: %s", restore_job_id, err)
|
||||||
unsub()
|
unsub()
|
||||||
|
|
||||||
|
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||||
|
"""Validate backup config.
|
||||||
|
|
||||||
|
Replace the core backup agent with the hassio default agent.
|
||||||
|
"""
|
||||||
|
core_agent_id = "backup.local"
|
||||||
|
create_backup = config.data.create_backup
|
||||||
|
if core_agent_id not in create_backup.agent_ids:
|
||||||
|
_LOGGER.debug("Backup settings don't need to be adjusted")
|
||||||
|
return
|
||||||
|
|
||||||
|
default_agent = await _default_agent(self._client)
|
||||||
|
_LOGGER.info("Adjusting backup settings to not include core backup location")
|
||||||
|
automatic_agents = [
|
||||||
|
agent_id if agent_id != core_agent_id else default_agent
|
||||||
|
for agent_id in create_backup.agent_ids
|
||||||
|
]
|
||||||
|
config.update(
|
||||||
|
create_backup=CreateBackupParametersDict(agent_ids=automatic_agents)
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_listen_job_events(
|
def _async_listen_job_events(
|
||||||
self, job_id: UUID, on_event: Callable[[Mapping[str, Any]], None]
|
self, job_id: UUID, on_event: Callable[[Mapping[str, Any]], None]
|
||||||
|
@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["apyhiveapi"],
|
"loggers": ["apyhiveapi"],
|
||||||
"requirements": ["pyhive-integration==1.0.1"]
|
"requirements": ["pyhive-integration==1.0.2"]
|
||||||
}
|
}
|
||||||
|
@ -432,6 +432,7 @@ def ws_expose_entity(
|
|||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
vol.Required("type"): "homeassistant/expose_entity/list",
|
vol.Required("type"): "homeassistant/expose_entity/list",
|
||||||
|
vol.Optional("assistant"): vol.In(KNOWN_ASSISTANTS),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def ws_list_exposed_entities(
|
def ws_list_exposed_entities(
|
||||||
@ -441,10 +442,18 @@ def ws_list_exposed_entities(
|
|||||||
result: dict[str, Any] = {}
|
result: dict[str, Any] = {}
|
||||||
|
|
||||||
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
|
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||||
|
required_assistant = msg.get("assistant")
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
for entity_id in chain(exposed_entities.entities, entity_registry.entities):
|
for entity_id in chain(exposed_entities.entities, entity_registry.entities):
|
||||||
result[entity_id] = {}
|
|
||||||
entity_settings = async_get_entity_settings(hass, entity_id)
|
entity_settings = async_get_entity_settings(hass, entity_id)
|
||||||
|
if required_assistant and (
|
||||||
|
(required_assistant not in entity_settings)
|
||||||
|
or (not entity_settings[required_assistant].get("should_expose"))
|
||||||
|
):
|
||||||
|
# Not exposed to required assistant
|
||||||
|
continue
|
||||||
|
|
||||||
|
result[entity_id] = {}
|
||||||
for assistant, settings in entity_settings.items():
|
for assistant, settings in entity_settings.items():
|
||||||
if "should_expose" not in settings:
|
if "should_expose" not in settings:
|
||||||
continue
|
continue
|
||||||
|
@ -107,7 +107,9 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
self._attr_effect_list = []
|
self._attr_effect_list = []
|
||||||
if effects := resource.effects:
|
if effects := resource.effects:
|
||||||
self._attr_effect_list = [
|
self._attr_effect_list = [
|
||||||
x.value for x in effects.status_values if x != EffectStatus.NO_EFFECT
|
x.value
|
||||||
|
for x in effects.status_values
|
||||||
|
if x not in (EffectStatus.NO_EFFECT, EffectStatus.UNKNOWN)
|
||||||
]
|
]
|
||||||
if timed_effects := resource.timed_effects:
|
if timed_effects := resource.timed_effects:
|
||||||
self._attr_effect_list += [
|
self._attr_effect_list += [
|
||||||
|
@ -39,7 +39,7 @@ set_preset_mode_with_end_datetime:
|
|||||||
select:
|
select:
|
||||||
options:
|
options:
|
||||||
- "away"
|
- "away"
|
||||||
- "Frost Guard"
|
- "frost_guard"
|
||||||
end_datetime:
|
end_datetime:
|
||||||
required: true
|
required: true
|
||||||
example: '"2019-04-20 05:04:20"'
|
example: '"2019-04-20 05:04:20"'
|
||||||
|
@ -235,7 +235,7 @@ class ONVIFDevice:
|
|||||||
LOGGER.debug("%s: Retrieving current device date/time", self.name)
|
LOGGER.debug("%s: Retrieving current device date/time", self.name)
|
||||||
try:
|
try:
|
||||||
device_time = await device_mgmt.GetSystemDateAndTime()
|
device_time = await device_mgmt.GetSystemDateAndTime()
|
||||||
except RequestError as err:
|
except (RequestError, Fault) as err:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Couldn't get device '%s' date/time. Error: %s", self.name, err
|
"Couldn't get device '%s' date/time. Error: %s", self.name, err
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from opower import Forecast, MeterType, UnitOfMeasure
|
from opower import Forecast, MeterType, UnitOfMeasure
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ from .coordinator import OpowerCoordinator
|
|||||||
class OpowerEntityDescription(SensorEntityDescription):
|
class OpowerEntityDescription(SensorEntityDescription):
|
||||||
"""Class describing Opower sensors entities."""
|
"""Class describing Opower sensors entities."""
|
||||||
|
|
||||||
value_fn: Callable[[Forecast], str | float]
|
value_fn: Callable[[Forecast], str | float | date]
|
||||||
|
|
||||||
|
|
||||||
# suggested_display_precision=0 for all sensors since
|
# suggested_display_precision=0 for all sensors since
|
||||||
@ -97,7 +98,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda data: str(data.start_date),
|
value_fn=lambda data: data.start_date,
|
||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_end_date",
|
key="elec_end_date",
|
||||||
@ -105,7 +106,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda data: str(data.end_date),
|
value_fn=lambda data: data.end_date,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||||
@ -169,7 +170,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda data: str(data.start_date),
|
value_fn=lambda data: data.start_date,
|
||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_end_date",
|
key="gas_end_date",
|
||||||
@ -177,7 +178,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda data: str(data.end_date),
|
value_fn=lambda data: data.end_date,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -247,7 +248,7 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
|||||||
self.utility_account_id = utility_account_id
|
self.utility_account_id = utility_account_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType | date:
|
||||||
"""Return the state."""
|
"""Return the state."""
|
||||||
if self.coordinator.data is not None:
|
if self.coordinator.data is not None:
|
||||||
return self.entity_description.value_fn(
|
return self.entity_description.value_fn(
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/prosegur",
|
"documentation": "https://www.home-assistant.io/integrations/prosegur",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyprosegur"],
|
"loggers": ["pyprosegur"],
|
||||||
"requirements": ["pyprosegur==0.0.9"]
|
"requirements": ["pyprosegur==0.0.13"]
|
||||||
}
|
}
|
||||||
|
@ -103,10 +103,10 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
|||||||
"""Handle incoming TCP push event."""
|
"""Handle incoming TCP push event."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def register_callback(self, unique_id: str, cmd_id: int) -> None:
|
def register_callback(self, callback_id: str, cmd_id: int) -> None:
|
||||||
"""Register callback for TCP push events."""
|
"""Register callback for TCP push events."""
|
||||||
self._host.api.baichuan.register_callback( # pragma: no cover
|
self._host.api.baichuan.register_callback( # pragma: no cover
|
||||||
unique_id, self._push_callback, cmd_id
|
callback_id, self._push_callback, cmd_id
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@ -114,23 +114,25 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
|||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
cmd_key = self.entity_description.cmd_key
|
cmd_key = self.entity_description.cmd_key
|
||||||
cmd_id = self.entity_description.cmd_id
|
cmd_id = self.entity_description.cmd_id
|
||||||
|
callback_id = f"{self.platform.domain}_{self._attr_unique_id}"
|
||||||
if cmd_key is not None:
|
if cmd_key is not None:
|
||||||
self._host.async_register_update_cmd(cmd_key)
|
self._host.async_register_update_cmd(cmd_key)
|
||||||
if cmd_id is not None:
|
if cmd_id is not None:
|
||||||
self.register_callback(self._attr_unique_id, cmd_id)
|
self.register_callback(callback_id, cmd_id)
|
||||||
# Privacy mode
|
# Privacy mode
|
||||||
self.register_callback(f"{self._attr_unique_id}_623", 623)
|
self.register_callback(f"{callback_id}_623", 623)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Entity removed."""
|
"""Entity removed."""
|
||||||
cmd_key = self.entity_description.cmd_key
|
cmd_key = self.entity_description.cmd_key
|
||||||
cmd_id = self.entity_description.cmd_id
|
cmd_id = self.entity_description.cmd_id
|
||||||
|
callback_id = f"{self.platform.domain}_{self._attr_unique_id}"
|
||||||
if cmd_key is not None:
|
if cmd_key is not None:
|
||||||
self._host.async_unregister_update_cmd(cmd_key)
|
self._host.async_unregister_update_cmd(cmd_key)
|
||||||
if cmd_id is not None:
|
if cmd_id is not None:
|
||||||
self._host.api.baichuan.unregister_callback(self._attr_unique_id)
|
self._host.api.baichuan.unregister_callback(callback_id)
|
||||||
# Privacy mode
|
# Privacy mode
|
||||||
self._host.api.baichuan.unregister_callback(f"{self._attr_unique_id}_623")
|
self._host.api.baichuan.unregister_callback(f"{callback_id}_623")
|
||||||
|
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
@ -189,10 +191,10 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return super().available and self._host.api.camera_online(self._channel)
|
return super().available and self._host.api.camera_online(self._channel)
|
||||||
|
|
||||||
def register_callback(self, unique_id: str, cmd_id: int) -> None:
|
def register_callback(self, callback_id: str, cmd_id: int) -> None:
|
||||||
"""Register callback for TCP push events."""
|
"""Register callback for TCP push events."""
|
||||||
self._host.api.baichuan.register_callback(
|
self._host.api.baichuan.register_callback(
|
||||||
unique_id, self._push_callback, cmd_id, self._channel
|
callback_id, self._push_callback, cmd_id, self._channel
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["reolink-aio==0.11.10"]
|
"requirements": ["reolink-aio==0.12.0"]
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ class ReolinkVODMediaSource(MediaSource):
|
|||||||
host = get_host(self.hass, config_entry_id)
|
host = get_host(self.hass, config_entry_id)
|
||||||
|
|
||||||
def get_vod_type() -> VodRequestType:
|
def get_vod_type() -> VodRequestType:
|
||||||
if filename.endswith(".mp4"):
|
if filename.endswith((".mp4", ".vref")):
|
||||||
if host.api.is_nvr:
|
if host.api.is_nvr:
|
||||||
return VodRequestType.DOWNLOAD
|
return VodRequestType.DOWNLOAD
|
||||||
return VodRequestType.PLAYBACK
|
return VodRequestType.PLAYBACK
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyrympro==0.0.8"]
|
"requirements": ["pyrympro==0.0.9"]
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"delivered": {
|
"delivered": {
|
||||||
"default": "mdi:package"
|
"default": "mdi:package"
|
||||||
},
|
},
|
||||||
"returned": {
|
"alert": {
|
||||||
"default": "mdi:package"
|
"default": "mdi:package"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
|
@ -11,7 +11,7 @@ get_packages:
|
|||||||
- "ready_to_be_picked_up"
|
- "ready_to_be_picked_up"
|
||||||
- "undelivered"
|
- "undelivered"
|
||||||
- "delivered"
|
- "delivered"
|
||||||
- "returned"
|
- "alert"
|
||||||
translation_key: package_state
|
translation_key: package_state
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
required: true
|
required: true
|
||||||
|
@ -57,8 +57,8 @@
|
|||||||
"delivered": {
|
"delivered": {
|
||||||
"name": "Delivered"
|
"name": "Delivered"
|
||||||
},
|
},
|
||||||
"returned": {
|
"alert": {
|
||||||
"name": "Returned"
|
"name": "Alert"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"name": "Package {name}"
|
"name": "Package {name}"
|
||||||
@ -104,7 +104,7 @@
|
|||||||
"ready_to_be_picked_up": "[%key:component::seventeentrack::entity::sensor::ready_to_be_picked_up::name%]",
|
"ready_to_be_picked_up": "[%key:component::seventeentrack::entity::sensor::ready_to_be_picked_up::name%]",
|
||||||
"undelivered": "[%key:component::seventeentrack::entity::sensor::undelivered::name%]",
|
"undelivered": "[%key:component::seventeentrack::entity::sensor::undelivered::name%]",
|
||||||
"delivered": "[%key:component::seventeentrack::entity::sensor::delivered::name%]",
|
"delivered": "[%key:component::seventeentrack::entity::sensor::delivered::name%]",
|
||||||
"returned": "[%key:component::seventeentrack::entity::sensor::returned::name%]"
|
"alert": "[%key:component::seventeentrack::entity::sensor::alert::name%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pymodbus", "pysmarty2"],
|
"loggers": ["pymodbus", "pysmarty2"],
|
||||||
"requirements": ["pysmarty2==0.10.1"]
|
"requirements": ["pysmarty2==0.10.2"]
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,7 @@ MODELS_TV_ONLY = (
|
|||||||
"BEAM",
|
"BEAM",
|
||||||
"PLAYBAR",
|
"PLAYBAR",
|
||||||
"PLAYBASE",
|
"PLAYBASE",
|
||||||
|
"ULTRA",
|
||||||
)
|
)
|
||||||
MODELS_LINEIN_AND_TV = ("AMP",)
|
MODELS_LINEIN_AND_TV = ("AMP",)
|
||||||
|
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tesla-fleet-api"],
|
"loggers": ["tesla-fleet-api"],
|
||||||
"requirements": ["tesla-fleet-api==0.9.8"]
|
"requirements": ["tesla-fleet-api==0.9.10"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tesla-fleet-api"],
|
"loggers": ["tesla-fleet-api"],
|
||||||
"requirements": ["tesla-fleet-api==0.9.8", "teslemetry-stream==0.6.6"]
|
"requirements": ["tesla-fleet-api==0.9.10", "teslemetry-stream==0.6.6"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tessie", "tesla-fleet-api"],
|
"loggers": ["tessie", "tesla-fleet-api"],
|
||||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.8"]
|
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.10"]
|
||||||
}
|
}
|
||||||
|
@ -256,7 +256,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
|||||||
TuyaBinarySensorEntityDescription(
|
TuyaBinarySensorEntityDescription(
|
||||||
key=DPCode.WATERSENSOR_STATE,
|
key=DPCode.WATERSENSOR_STATE,
|
||||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||||
on_value="alarm",
|
on_value={"1", "alarm"},
|
||||||
),
|
),
|
||||||
TAMPER_BINARY_SENSOR,
|
TAMPER_BINARY_SENSOR,
|
||||||
),
|
),
|
||||||
|
@ -12,5 +12,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyvesync"],
|
"loggers": ["pyvesync"],
|
||||||
"requirements": ["pyvesync==2.1.17"]
|
"requirements": ["pyvesync==2.1.18"]
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2025
|
MAJOR_VERSION: Final = 2025
|
||||||
MINOR_VERSION: Final = 2
|
MINOR_VERSION: Final = 2
|
||||||
PATCH_VERSION: Final = "4"
|
PATCH_VERSION: Final = "5"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
||||||
|
@ -37,7 +37,7 @@ habluetooth==3.21.1
|
|||||||
hass-nabucasa==0.92.0
|
hass-nabucasa==0.92.0
|
||||||
hassil==2.2.3
|
hassil==2.2.3
|
||||||
home-assistant-bluetooth==1.13.0
|
home-assistant-bluetooth==1.13.0
|
||||||
home-assistant-frontend==20250214.0
|
home-assistant-frontend==20250221.0
|
||||||
home-assistant-intents==2025.2.5
|
home-assistant-intents==2025.2.5
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2025.2.4"
|
version = "2025.2.5"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
22
requirements_all.txt
generated
22
requirements_all.txt
generated
@ -428,7 +428,7 @@ aiowithings==3.1.5
|
|||||||
aioymaps==1.2.5
|
aioymaps==1.2.5
|
||||||
|
|
||||||
# homeassistant.components.airgradient
|
# homeassistant.components.airgradient
|
||||||
airgradient==0.9.1
|
airgradient==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.airly
|
# homeassistant.components.airly
|
||||||
airly==1.1.0
|
airly==1.1.0
|
||||||
@ -747,7 +747,7 @@ debugpy==1.8.11
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==12.1.0
|
deebot-client==12.2.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -1143,7 +1143,7 @@ hole==0.8.0
|
|||||||
holidays==0.66
|
holidays==0.66
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250214.0
|
home-assistant-frontend==20250221.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.2.5
|
home-assistant-intents==2025.2.5
|
||||||
@ -1969,7 +1969,7 @@ pyforked-daapd==0.1.14
|
|||||||
pyfreedompro==1.1.0
|
pyfreedompro==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.fritzbox
|
# homeassistant.components.fritzbox
|
||||||
pyfritzhome==0.6.14
|
pyfritzhome==0.6.15
|
||||||
|
|
||||||
# homeassistant.components.ifttt
|
# homeassistant.components.ifttt
|
||||||
pyfttt==0.3
|
pyfttt==0.3
|
||||||
@ -1990,7 +1990,7 @@ pyhaversion==22.8.0
|
|||||||
pyheos==1.0.2
|
pyheos==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.hive
|
# homeassistant.components.hive
|
||||||
pyhive-integration==1.0.1
|
pyhive-integration==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.homematic
|
# homeassistant.components.homematic
|
||||||
pyhomematic==0.1.77
|
pyhomematic==0.1.77
|
||||||
@ -2217,7 +2217,7 @@ pypoint==3.0.0
|
|||||||
pyprof2calltree==1.4.5
|
pyprof2calltree==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.prosegur
|
# homeassistant.components.prosegur
|
||||||
pyprosegur==0.0.9
|
pyprosegur==0.0.13
|
||||||
|
|
||||||
# homeassistant.components.prusalink
|
# homeassistant.components.prusalink
|
||||||
pyprusalink==2.1.1
|
pyprusalink==2.1.1
|
||||||
@ -2253,7 +2253,7 @@ pyrituals==0.0.6
|
|||||||
pyroute2==0.7.5
|
pyroute2==0.7.5
|
||||||
|
|
||||||
# homeassistant.components.rympro
|
# homeassistant.components.rympro
|
||||||
pyrympro==0.0.8
|
pyrympro==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.sabnzbd
|
# homeassistant.components.sabnzbd
|
||||||
pysabnzbd==1.1.1
|
pysabnzbd==1.1.1
|
||||||
@ -2304,7 +2304,7 @@ pysmartapp==0.3.5
|
|||||||
pysmartthings==0.7.8
|
pysmartthings==0.7.8
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.1
|
pysmarty2==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.edl21
|
# homeassistant.components.edl21
|
||||||
pysml==0.0.12
|
pysml==0.0.12
|
||||||
@ -2513,7 +2513,7 @@ pyvera==0.3.15
|
|||||||
pyversasense==0.0.6
|
pyversasense==0.0.6
|
||||||
|
|
||||||
# homeassistant.components.vesync
|
# homeassistant.components.vesync
|
||||||
pyvesync==2.1.17
|
pyvesync==2.1.18
|
||||||
|
|
||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.1.61
|
pyvizio==0.1.61
|
||||||
@ -2603,7 +2603,7 @@ renault-api==0.2.9
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.11.10
|
reolink-aio==0.12.0
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
@ -2854,7 +2854,7 @@ temperusb==1.6.1
|
|||||||
# homeassistant.components.tesla_fleet
|
# homeassistant.components.tesla_fleet
|
||||||
# homeassistant.components.teslemetry
|
# homeassistant.components.teslemetry
|
||||||
# homeassistant.components.tessie
|
# homeassistant.components.tessie
|
||||||
tesla-fleet-api==0.9.8
|
tesla-fleet-api==0.9.10
|
||||||
|
|
||||||
# homeassistant.components.powerwall
|
# homeassistant.components.powerwall
|
||||||
tesla-powerwall==0.5.2
|
tesla-powerwall==0.5.2
|
||||||
|
22
requirements_test_all.txt
generated
22
requirements_test_all.txt
generated
@ -410,7 +410,7 @@ aiowithings==3.1.5
|
|||||||
aioymaps==1.2.5
|
aioymaps==1.2.5
|
||||||
|
|
||||||
# homeassistant.components.airgradient
|
# homeassistant.components.airgradient
|
||||||
airgradient==0.9.1
|
airgradient==0.9.2
|
||||||
|
|
||||||
# homeassistant.components.airly
|
# homeassistant.components.airly
|
||||||
airly==1.1.0
|
airly==1.1.0
|
||||||
@ -637,7 +637,7 @@ dbus-fast==2.33.0
|
|||||||
debugpy==1.8.11
|
debugpy==1.8.11
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==12.1.0
|
deebot-client==12.2.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -972,7 +972,7 @@ hole==0.8.0
|
|||||||
holidays==0.66
|
holidays==0.66
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250214.0
|
home-assistant-frontend==20250221.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.2.5
|
home-assistant-intents==2025.2.5
|
||||||
@ -1604,7 +1604,7 @@ pyforked-daapd==0.1.14
|
|||||||
pyfreedompro==1.1.0
|
pyfreedompro==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.fritzbox
|
# homeassistant.components.fritzbox
|
||||||
pyfritzhome==0.6.14
|
pyfritzhome==0.6.15
|
||||||
|
|
||||||
# homeassistant.components.ifttt
|
# homeassistant.components.ifttt
|
||||||
pyfttt==0.3
|
pyfttt==0.3
|
||||||
@ -1619,7 +1619,7 @@ pyhaversion==22.8.0
|
|||||||
pyheos==1.0.2
|
pyheos==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.hive
|
# homeassistant.components.hive
|
||||||
pyhive-integration==1.0.1
|
pyhive-integration==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.homematic
|
# homeassistant.components.homematic
|
||||||
pyhomematic==0.1.77
|
pyhomematic==0.1.77
|
||||||
@ -1810,7 +1810,7 @@ pypoint==3.0.0
|
|||||||
pyprof2calltree==1.4.5
|
pyprof2calltree==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.prosegur
|
# homeassistant.components.prosegur
|
||||||
pyprosegur==0.0.9
|
pyprosegur==0.0.13
|
||||||
|
|
||||||
# homeassistant.components.prusalink
|
# homeassistant.components.prusalink
|
||||||
pyprusalink==2.1.1
|
pyprusalink==2.1.1
|
||||||
@ -1837,7 +1837,7 @@ pyrituals==0.0.6
|
|||||||
pyroute2==0.7.5
|
pyroute2==0.7.5
|
||||||
|
|
||||||
# homeassistant.components.rympro
|
# homeassistant.components.rympro
|
||||||
pyrympro==0.0.8
|
pyrympro==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.sabnzbd
|
# homeassistant.components.sabnzbd
|
||||||
pysabnzbd==1.1.1
|
pysabnzbd==1.1.1
|
||||||
@ -1876,7 +1876,7 @@ pysmartapp==0.3.5
|
|||||||
pysmartthings==0.7.8
|
pysmartthings==0.7.8
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.1
|
pysmarty2==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.edl21
|
# homeassistant.components.edl21
|
||||||
pysml==0.0.12
|
pysml==0.0.12
|
||||||
@ -2031,7 +2031,7 @@ pyuptimerobot==22.2.0
|
|||||||
pyvera==0.3.15
|
pyvera==0.3.15
|
||||||
|
|
||||||
# homeassistant.components.vesync
|
# homeassistant.components.vesync
|
||||||
pyvesync==2.1.17
|
pyvesync==2.1.18
|
||||||
|
|
||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.1.61
|
pyvizio==0.1.61
|
||||||
@ -2106,7 +2106,7 @@ renault-api==0.2.9
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.11.10
|
reolink-aio==0.12.0
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.66
|
rflink==0.0.66
|
||||||
@ -2294,7 +2294,7 @@ temperusb==1.6.1
|
|||||||
# homeassistant.components.tesla_fleet
|
# homeassistant.components.tesla_fleet
|
||||||
# homeassistant.components.teslemetry
|
# homeassistant.components.teslemetry
|
||||||
# homeassistant.components.tessie
|
# homeassistant.components.tessie
|
||||||
tesla-fleet-api==0.9.8
|
tesla-fleet-api==0.9.10
|
||||||
|
|
||||||
# homeassistant.components.powerwall
|
# homeassistant.components.powerwall
|
||||||
tesla-powerwall==0.5.2
|
tesla-powerwall==0.5.2
|
||||||
|
@ -1821,23 +1821,6 @@ async def snapshot_platform(
|
|||||||
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
|
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
|
||||||
|
|
||||||
|
|
||||||
def reset_translation_cache(hass: HomeAssistant, components: list[str]) -> None:
|
|
||||||
"""Reset translation cache for specified components.
|
|
||||||
|
|
||||||
Use this if you are mocking a core component (for example via
|
|
||||||
mock_integration), to ensure that the mocked translations are not
|
|
||||||
persisted in the shared session cache.
|
|
||||||
"""
|
|
||||||
translations_cache = translation._async_get_translations_cache(hass)
|
|
||||||
for loaded_components in translations_cache.cache_data.loaded.values():
|
|
||||||
for component_to_unload in components:
|
|
||||||
loaded_components.discard(component_to_unload)
|
|
||||||
for loaded_categories in translations_cache.cache_data.cache.values():
|
|
||||||
for loaded_components in loaded_categories.values():
|
|
||||||
for component_to_unload in components:
|
|
||||||
loaded_components.pop(component_to_unload, None)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_quality_scale(integration: str) -> dict[str, QualityScaleStatus]:
|
def get_quality_scale(integration: str) -> dict[str, QualityScaleStatus]:
|
||||||
"""Load quality scale for integration."""
|
"""Load quality scale for integration."""
|
||||||
|
@ -25,13 +25,13 @@
|
|||||||
'nitrogen_index': 1,
|
'nitrogen_index': 1,
|
||||||
'pm003_count': 270,
|
'pm003_count': 270,
|
||||||
'pm01': 22,
|
'pm01': 22,
|
||||||
'pm02': 34,
|
'pm02': 34.0,
|
||||||
'pm10': 41,
|
'pm10': 41,
|
||||||
'raw_ambient_temperature': 27.96,
|
'raw_ambient_temperature': 27.96,
|
||||||
'raw_nitrogen': 16931,
|
'raw_nitrogen': 16931.0,
|
||||||
'raw_pm02': 34,
|
'raw_pm02': 34,
|
||||||
'raw_relative_humidity': 48.0,
|
'raw_relative_humidity': 48.0,
|
||||||
'raw_total_volatile_organic_component': 31792,
|
'raw_total_volatile_organic_component': 31792.0,
|
||||||
'rco2': 778,
|
'rco2': 778,
|
||||||
'relative_humidity': 47.0,
|
'relative_humidity': 47.0,
|
||||||
'serial_number': '84fce612f5b8',
|
'serial_number': '84fce612f5b8',
|
||||||
|
@ -710,7 +710,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '34',
|
'state': '34.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[indoor][sensor.airgradient_raw_nox-entry]
|
# name: test_all_entities[indoor][sensor.airgradient_raw_nox-entry]
|
||||||
@ -760,7 +760,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '16931',
|
'state': '16931.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[indoor][sensor.airgradient_raw_pm2_5-entry]
|
# name: test_all_entities[indoor][sensor.airgradient_raw_pm2_5-entry]
|
||||||
@ -861,7 +861,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '31792',
|
'state': '31792.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[indoor][sensor.airgradient_signal_strength-entry]
|
# name: test_all_entities[indoor][sensor.airgradient_signal_strength-entry]
|
||||||
@ -1255,7 +1255,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '16359',
|
'state': '16359.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-entry]
|
# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-entry]
|
||||||
@ -1305,7 +1305,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '30802',
|
'state': '30802.0',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-entry]
|
# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-entry]
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -84,11 +84,100 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_store_migration[store_data1]
|
# name: test_store_migration[store_data1]
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'backups': list([
|
||||||
|
dict({
|
||||||
|
'backup_id': 'abc123',
|
||||||
|
'failed_agent_ids': list([
|
||||||
|
'test.remote',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'state': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'key': 'backup',
|
||||||
|
'minor_version': 4,
|
||||||
|
'version': 1,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_store_migration[store_data1].1
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'backups': list([
|
||||||
|
dict({
|
||||||
|
'backup_id': 'abc123',
|
||||||
|
'failed_agent_ids': list([
|
||||||
|
'test.remote',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'state': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'key': 'backup',
|
||||||
|
'minor_version': 4,
|
||||||
|
'version': 1,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_store_migration[store_data2]
|
||||||
dict({
|
dict({
|
||||||
'data': dict({
|
'data': dict({
|
||||||
'backups': list([
|
'backups': list([
|
||||||
@ -131,11 +220,11 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_store_migration[store_data1].1
|
# name: test_store_migration[store_data2].1
|
||||||
dict({
|
dict({
|
||||||
'data': dict({
|
'data': dict({
|
||||||
'backups': list([
|
'backups': list([
|
||||||
@ -179,7 +268,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
@ -251,7 +251,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data0]
|
# name: test_config_load_config_info[with_hassio-storage_data0]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -288,7 +288,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data1]
|
# name: test_config_load_config_info[with_hassio-storage_data1]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -337,7 +337,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data2]
|
# name: test_config_load_config_info[with_hassio-storage_data2]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -375,7 +375,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data3]
|
# name: test_config_load_config_info[with_hassio-storage_data3]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -413,7 +413,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data4]
|
# name: test_config_load_config_info[with_hassio-storage_data4]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -452,7 +452,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data5]
|
# name: test_config_load_config_info[with_hassio-storage_data5]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -490,7 +490,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data6]
|
# name: test_config_load_config_info[with_hassio-storage_data6]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -530,7 +530,7 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_config_info[storage_data7]
|
# name: test_config_load_config_info[with_hassio-storage_data7]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': dict({
|
'result': dict({
|
||||||
@ -576,6 +576,484 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_config_load_config_info[with_hassio-storage_data8]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'hassio.local',
|
||||||
|
'hassio.share',
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[with_hassio-storage_data9]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'hassio.local',
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data0]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data1]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': 'test-name',
|
||||||
|
'password': 'test-password',
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': '2024-10-26T04:45:00+01:00',
|
||||||
|
'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00',
|
||||||
|
'next_automatic_backup': '2024-11-14T04:55:00+01:00',
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': 3,
|
||||||
|
'days': 7,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
'mon',
|
||||||
|
'tue',
|
||||||
|
'wed',
|
||||||
|
'thu',
|
||||||
|
'fri',
|
||||||
|
'sat',
|
||||||
|
'sun',
|
||||||
|
]),
|
||||||
|
'recurrence': 'custom_days',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data2]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': 3,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data3]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': '2024-10-27T04:45:00+01:00',
|
||||||
|
'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00',
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': 7,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data4]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': '2024-11-18T04:55:00+01:00',
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
'mon',
|
||||||
|
]),
|
||||||
|
'recurrence': 'custom_days',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data5]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data6]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': '2024-11-17T04:55:00+01:00',
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
'mon',
|
||||||
|
'sun',
|
||||||
|
]),
|
||||||
|
'recurrence': 'custom_days',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data7]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
'test-agent1': dict({
|
||||||
|
'protected': True,
|
||||||
|
}),
|
||||||
|
'test-agent2': dict({
|
||||||
|
'protected': False,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': '2024-11-17T04:55:00+01:00',
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
'mon',
|
||||||
|
'sun',
|
||||||
|
]),
|
||||||
|
'recurrence': 'custom_days',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data8]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'backup.local',
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[without_hassio-storage_data9]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'backup.local',
|
||||||
|
'test-agent',
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': False,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_config_update[commands0]
|
# name: test_config_update[commands0]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
@ -686,7 +1164,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -800,7 +1278,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -914,7 +1392,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1038,7 +1516,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1205,7 +1683,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1319,7 +1797,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1435,7 +1913,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1549,7 +2027,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1667,7 +2145,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1789,7 +2267,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -1903,7 +2381,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -2017,7 +2495,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -2131,7 +2609,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -2245,7 +2723,7 @@
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'key': 'backup',
|
'key': 'backup',
|
||||||
'minor_version': 3,
|
'minor_version': 4,
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -2323,6 +2801,154 @@
|
|||||||
'type': 'result',
|
'type': 'result',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_config_update_errors[command10]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_update_errors[command10].1
|
||||||
|
dict({
|
||||||
|
'id': 3,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_update_errors[command11]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_update_errors[command11].1
|
||||||
|
dict({
|
||||||
|
'id': 3,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_config_update_errors[command1]
|
# name: test_config_update_errors[command1]
|
||||||
dict({
|
dict({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
|
@ -57,6 +57,38 @@ def mock_delay_save() -> Generator[None]:
|
|||||||
"key": DOMAIN,
|
"key": DOMAIN,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"backups": [
|
||||||
|
{
|
||||||
|
"backup_id": "abc123",
|
||||||
|
"failed_agent_ids": ["test.remote"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": [],
|
||||||
|
"include_addons": None,
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": True,
|
||||||
|
"include_folders": None,
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"retention": {
|
||||||
|
"copies": 0,
|
||||||
|
"days": 0,
|
||||||
|
},
|
||||||
|
"schedule": {
|
||||||
|
"state": "never",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"backups": [
|
"backups": [
|
||||||
|
@ -89,6 +89,28 @@ from tests.common import get_fixture_path
|
|||||||
size=1234,
|
size=1234,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# Check the backup_request_date is used as date if present
|
||||||
|
(
|
||||||
|
b'{"compressed":true,"date":"2024-12-01T00:00:00.000000-00:00","homeassistant":'
|
||||||
|
b'{"exclude_database":true,"version":"2024.12.0.dev0"},"name":"test",'
|
||||||
|
b'"extra":{"supervisor.backup_request_date":"2025-12-01T00:00:00.000000-00:00"},'
|
||||||
|
b'"protected":true,"slug":"455645fe","type":"partial","version":2}',
|
||||||
|
AgentBackup(
|
||||||
|
addons=[],
|
||||||
|
backup_id="455645fe",
|
||||||
|
date="2025-12-01T00:00:00.000000-00:00",
|
||||||
|
database_included=False,
|
||||||
|
extra_metadata={
|
||||||
|
"supervisor.backup_request_date": "2025-12-01T00:00:00.000000-00:00"
|
||||||
|
},
|
||||||
|
folders=[],
|
||||||
|
homeassistant_included=True,
|
||||||
|
homeassistant_version="2024.12.0.dev0",
|
||||||
|
name="test",
|
||||||
|
protected=True,
|
||||||
|
size=1234,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_read_backup(backup_json_content: bytes, expected_backup: AgentBackup) -> None:
|
def test_read_backup(backup_json_content: bytes, expected_backup: AgentBackup) -> None:
|
||||||
|
@ -46,10 +46,10 @@ BACKUP_CALL = call(
|
|||||||
agent_ids=["test.test-agent"],
|
agent_ids=["test.test-agent"],
|
||||||
backup_name="test-name",
|
backup_name="test-name",
|
||||||
extra_metadata={"instance_id": ANY, "with_automatic_settings": True},
|
extra_metadata={"instance_id": ANY, "with_automatic_settings": True},
|
||||||
include_addons=["test-addon"],
|
include_addons=[],
|
||||||
include_all_addons=False,
|
include_all_addons=False,
|
||||||
include_database=True,
|
include_database=True,
|
||||||
include_folders=["media"],
|
include_folders=None,
|
||||||
include_homeassistant=True,
|
include_homeassistant=True,
|
||||||
password="test-password",
|
password="test-password",
|
||||||
on_progress=ANY,
|
on_progress=ANY,
|
||||||
@ -1126,25 +1126,96 @@ async def test_agents_info(
|
|||||||
"minor_version": store.STORAGE_VERSION_MINOR,
|
"minor_version": store.STORAGE_VERSION_MINOR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"backup": {
|
||||||
|
"data": {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["hassio.local", "hassio.share", "test-agent"],
|
||||||
|
"include_addons": None,
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": False,
|
||||||
|
"include_folders": None,
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"schedule": {
|
||||||
|
"days": [],
|
||||||
|
"recurrence": "never",
|
||||||
|
"state": "never",
|
||||||
|
"time": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": store.STORAGE_VERSION,
|
||||||
|
"minor_version": store.STORAGE_VERSION_MINOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"backup": {
|
||||||
|
"data": {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["backup.local", "test-agent"],
|
||||||
|
"include_addons": None,
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": False,
|
||||||
|
"include_folders": None,
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"schedule": {
|
||||||
|
"days": [],
|
||||||
|
"recurrence": "never",
|
||||||
|
"state": "never",
|
||||||
|
"time": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": store.STORAGE_VERSION,
|
||||||
|
"minor_version": store.STORAGE_VERSION_MINOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("with_hassio"),
|
||||||
|
[
|
||||||
|
pytest.param(True, id="with_hassio"),
|
||||||
|
pytest.param(False, id="without_hassio"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("supervisor_client")
|
||||||
@patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600))
|
@patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600))
|
||||||
async def test_config_info(
|
async def test_config_load_config_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
hass_storage: dict[str, Any],
|
hass_storage: dict[str, Any],
|
||||||
|
with_hassio: bool,
|
||||||
storage_data: dict[str, Any] | None,
|
storage_data: dict[str, Any] | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test getting backup config info."""
|
"""Test loading stored backup config and reading it via config/info."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||||
freezer.move_to("2024-11-13T12:01:00+01:00")
|
freezer.move_to("2024-11-13T12:01:00+01:00")
|
||||||
|
|
||||||
hass_storage.update(storage_data)
|
hass_storage.update(storage_data)
|
||||||
|
|
||||||
await setup_backup_integration(hass)
|
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
await client.send_json_auto_id({"type": "backup/config/info"})
|
await client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
@ -1366,6 +1437,14 @@ async def test_config_update(
|
|||||||
"type": "backup/config/update",
|
"type": "backup/config/update",
|
||||||
"agents": {"test-agent1": {"favorite": True}},
|
"agents": {"test-agent1": {"favorite": True}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "backup/config/update",
|
||||||
|
"retention": {"copies": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "backup/config/update",
|
||||||
|
"retention": {"days": 0},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_config_update_errors(
|
async def test_config_update_errors(
|
||||||
@ -1702,10 +1781,10 @@ async def test_config_schedule_logic(
|
|||||||
"agents": {},
|
"agents": {},
|
||||||
"create_backup": {
|
"create_backup": {
|
||||||
"agent_ids": ["test.test-agent"],
|
"agent_ids": ["test.test-agent"],
|
||||||
"include_addons": ["test-addon"],
|
"include_addons": [],
|
||||||
"include_all_addons": False,
|
"include_all_addons": False,
|
||||||
"include_database": True,
|
"include_database": True,
|
||||||
"include_folders": ["media"],
|
"include_folders": [],
|
||||||
"name": "test-name",
|
"name": "test-name",
|
||||||
"password": "test-password",
|
"password": "test-password",
|
||||||
},
|
},
|
||||||
@ -2163,7 +2242,7 @@ async def test_config_schedule_logic(
|
|||||||
{
|
{
|
||||||
"type": "backup/config/update",
|
"type": "backup/config/update",
|
||||||
"create_backup": {"agent_ids": ["test.test-agent"]},
|
"create_backup": {"agent_ids": ["test.test-agent"]},
|
||||||
"retention": {"copies": 0, "days": None},
|
"retention": {"copies": 1, "days": None},
|
||||||
"schedule": {"recurrence": "daily"},
|
"schedule": {"recurrence": "daily"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2237,7 +2316,7 @@ async def test_config_schedule_logic(
|
|||||||
{
|
{
|
||||||
"type": "backup/config/update",
|
"type": "backup/config/update",
|
||||||
"create_backup": {"agent_ids": ["test.test-agent"]},
|
"create_backup": {"agent_ids": ["test.test-agent"]},
|
||||||
"retention": {"copies": 0, "days": None},
|
"retention": {"copies": 1, "days": None},
|
||||||
"schedule": {"recurrence": "daily"},
|
"schedule": {"recurrence": "daily"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2306,7 +2385,7 @@ async def test_config_schedule_logic(
|
|||||||
{
|
{
|
||||||
"type": "backup/config/update",
|
"type": "backup/config/update",
|
||||||
"create_backup": {"agent_ids": ["test.test-agent"]},
|
"create_backup": {"agent_ids": ["test.test-agent"]},
|
||||||
"retention": {"copies": 0, "days": None},
|
"retention": {"copies": 1, "days": None},
|
||||||
"schedule": {"recurrence": "daily"},
|
"schedule": {"recurrence": "daily"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3027,7 +3106,7 @@ async def test_config_retention_copies_logic_manual_backup(
|
|||||||
{
|
{
|
||||||
"type": "backup/config/update",
|
"type": "backup/config/update",
|
||||||
"create_backup": {"agent_ids": ["test-agent"]},
|
"create_backup": {"agent_ids": ["test-agent"]},
|
||||||
"retention": {"copies": None, "days": 0},
|
"retention": {"copies": None, "days": 1},
|
||||||
"schedule": {"recurrence": "never"},
|
"schedule": {"recurrence": "never"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
from collections.abc import AsyncGenerator, Generator
|
from collections.abc import AsyncGenerator, Generator
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import Mock, PropertyMock, patch
|
from unittest.mock import ANY, Mock, PropertyMock, patch
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from hass_nabucasa import CloudError
|
from hass_nabucasa import CloudError
|
||||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||||
from hass_nabucasa.files import FilesError
|
from hass_nabucasa.files import FilesError, StorageType
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.backup import (
|
from homeassistant.components.backup import (
|
||||||
@ -90,7 +90,26 @@ def mock_list_files() -> Generator[MagicMock]:
|
|||||||
"size": 34519040,
|
"size": 34519040,
|
||||||
"storage-type": "backup",
|
"storage-type": "backup",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"Key": "462e16810d6841228828d9dd2f9e341f.tar",
|
||||||
|
"LastModified": "2024-11-22T10:49:01.182Z",
|
||||||
|
"Size": 34519040,
|
||||||
|
"Metadata": {
|
||||||
|
"addons": [],
|
||||||
|
"backup_id": "23e64aed",
|
||||||
|
"date": "2024-11-22T11:48:48.727189+01:00",
|
||||||
|
"database_included": True,
|
||||||
|
"extra_metadata": {},
|
||||||
|
"folders": [],
|
||||||
|
"homeassistant_included": True,
|
||||||
|
"homeassistant_version": "2024.12.0.dev0",
|
||||||
|
"name": "Core 2024.12.0.dev0",
|
||||||
|
"protected": False,
|
||||||
|
"size": 34519040,
|
||||||
|
"storage-type": "backup",
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
yield list_files
|
yield list_files
|
||||||
|
|
||||||
@ -148,7 +167,21 @@ async def test_agents_list_backups(
|
|||||||
"name": "Core 2024.12.0.dev0",
|
"name": "Core 2024.12.0.dev0",
|
||||||
"failed_agent_ids": [],
|
"failed_agent_ids": [],
|
||||||
"with_automatic_settings": None,
|
"with_automatic_settings": None,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"addons": [],
|
||||||
|
"agents": {"cloud.cloud": {"protected": False, "size": 34519040}},
|
||||||
|
"backup_id": "23e64aed",
|
||||||
|
"date": "2024-11-22T11:48:48.727189+01:00",
|
||||||
|
"database_included": True,
|
||||||
|
"extra_metadata": {},
|
||||||
|
"folders": [],
|
||||||
|
"homeassistant_included": True,
|
||||||
|
"homeassistant_version": "2024.12.0.dev0",
|
||||||
|
"name": "Core 2024.12.0.dev0",
|
||||||
|
"failed_agent_ids": [],
|
||||||
|
"with_automatic_settings": None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +275,10 @@ async def test_agents_download(
|
|||||||
resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud")
|
resp = await client.get(f"/api/backup/download/{backup_id}?agent_id=cloud.cloud")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert await resp.content.read() == b"backup data"
|
assert await resp.content.read() == b"backup data"
|
||||||
|
cloud.files.download.assert_called_once_with(
|
||||||
|
filename="462e16810d6841228828d9dd2f9e341e.tar",
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
|
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
|
||||||
@ -317,7 +354,14 @@ async def test_agents_upload(
|
|||||||
data={"file": StringIO(backup_data)},
|
data={"file": StringIO(backup_data)},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(cloud.files.upload.mock_calls) == 1
|
cloud.files.upload.assert_called_once_with(
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
open_stream=ANY,
|
||||||
|
filename=f"{cloud.client.prefs.instance_id}.tar",
|
||||||
|
base64md5hash=ANY,
|
||||||
|
metadata=ANY,
|
||||||
|
size=ANY,
|
||||||
|
)
|
||||||
metadata = cloud.files.upload.mock_calls[-1].kwargs["metadata"]
|
metadata = cloud.files.upload.mock_calls[-1].kwargs["metadata"]
|
||||||
assert metadata["backup_id"] == backup_id
|
assert metadata["backup_id"] == backup_id
|
||||||
|
|
||||||
@ -552,6 +596,7 @@ async def test_agents_upload_wrong_size(
|
|||||||
async def test_agents_delete(
|
async def test_agents_delete(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
cloud: Mock,
|
||||||
mock_delete_file: Mock,
|
mock_delete_file: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test agent delete backup."""
|
"""Test agent delete backup."""
|
||||||
@ -568,7 +613,11 @@ async def test_agents_delete(
|
|||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {"agent_errors": {}}
|
assert response["result"] == {"agent_errors": {}}
|
||||||
mock_delete_file.assert_called_once()
|
mock_delete_file.assert_called_once_with(
|
||||||
|
cloud,
|
||||||
|
filename="462e16810d6841228828d9dd2f9e341e.tar",
|
||||||
|
storage_type=StorageType.BACKUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side_effect", [ClientError, CloudError])
|
@pytest.mark.parametrize("side_effect", [ClientError, CloudError])
|
||||||
|
@ -529,6 +529,7 @@ def resolution_suggestions_for_issue_fixture(supervisor_client: AsyncMock) -> As
|
|||||||
def supervisor_client() -> Generator[AsyncMock]:
|
def supervisor_client() -> Generator[AsyncMock]:
|
||||||
"""Mock the supervisor client."""
|
"""Mock the supervisor client."""
|
||||||
mounts_info_mock = AsyncMock(spec_set=["default_backup_mount", "mounts"])
|
mounts_info_mock = AsyncMock(spec_set=["default_backup_mount", "mounts"])
|
||||||
|
mounts_info_mock.default_backup_mount = None
|
||||||
mounts_info_mock.mounts = []
|
mounts_info_mock.mounts = []
|
||||||
supervisor_client = AsyncMock()
|
supervisor_client = AsyncMock()
|
||||||
supervisor_client.addons = AsyncMock()
|
supervisor_client.addons = AsyncMock()
|
||||||
|
@ -23,7 +23,12 @@ from homeassistant.components.climate import (
|
|||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.components.fritzbox.climate import PRESET_HOLIDAY, PRESET_SUMMER
|
from homeassistant.components.fritzbox.climate import (
|
||||||
|
OFF_API_TEMPERATURE,
|
||||||
|
ON_API_TEMPERATURE,
|
||||||
|
PRESET_HOLIDAY,
|
||||||
|
PRESET_SUMMER,
|
||||||
|
)
|
||||||
from homeassistant.components.fritzbox.const import (
|
from homeassistant.components.fritzbox.const import (
|
||||||
ATTR_STATE_BATTERY_LOW,
|
ATTR_STATE_BATTERY_LOW,
|
||||||
ATTR_STATE_HOLIDAY_MODE,
|
ATTR_STATE_HOLIDAY_MODE,
|
||||||
@ -367,9 +372,23 @@ async def test_set_hvac_mode(
|
|||||||
assert device.set_target_temperature.call_args_list == expected_call_args
|
assert device.set_target_temperature.call_args_list == expected_call_args
|
||||||
|
|
||||||
|
|
||||||
async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
("comfort_temperature", "expected_call_args"),
|
||||||
|
[
|
||||||
|
(20, [call(20, True)]),
|
||||||
|
(28, [call(28, True)]),
|
||||||
|
(ON_API_TEMPERATURE, [call(30, True)]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_set_preset_mode_comfort(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
fritz: Mock,
|
||||||
|
comfort_temperature: int,
|
||||||
|
expected_call_args: list[_Call],
|
||||||
|
) -> None:
|
||||||
"""Test setting preset mode."""
|
"""Test setting preset mode."""
|
||||||
device = FritzDeviceClimateMock()
|
device = FritzDeviceClimateMock()
|
||||||
|
device.comfort_temperature = comfort_temperature
|
||||||
assert await setup_config_entry(
|
assert await setup_config_entry(
|
||||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||||
)
|
)
|
||||||
@ -380,12 +399,27 @@ async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock) -> None
|
|||||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_COMFORT},
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_COMFORT},
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
assert device.set_target_temperature.call_args_list == [call(22, True)]
|
assert device.set_target_temperature.call_count == len(expected_call_args)
|
||||||
|
assert device.set_target_temperature.call_args_list == expected_call_args
|
||||||
|
|
||||||
|
|
||||||
async def test_set_preset_mode_eco(hass: HomeAssistant, fritz: Mock) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
("eco_temperature", "expected_call_args"),
|
||||||
|
[
|
||||||
|
(20, [call(20, True)]),
|
||||||
|
(16, [call(16, True)]),
|
||||||
|
(OFF_API_TEMPERATURE, [call(0, True)]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_set_preset_mode_eco(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
fritz: Mock,
|
||||||
|
eco_temperature: int,
|
||||||
|
expected_call_args: list[_Call],
|
||||||
|
) -> None:
|
||||||
"""Test setting preset mode."""
|
"""Test setting preset mode."""
|
||||||
device = FritzDeviceClimateMock()
|
device = FritzDeviceClimateMock()
|
||||||
|
device.eco_temperature = eco_temperature
|
||||||
assert await setup_config_entry(
|
assert await setup_config_entry(
|
||||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||||
)
|
)
|
||||||
@ -396,7 +430,8 @@ async def test_set_preset_mode_eco(hass: HomeAssistant, fritz: Mock) -> None:
|
|||||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO},
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO},
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
assert device.set_target_temperature.call_args_list == [call(16, True)]
|
assert device.set_target_temperature.call_count == len(expected_call_args)
|
||||||
|
assert device.set_target_temperature.call_args_list == expected_call_args
|
||||||
|
|
||||||
|
|
||||||
async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||||
|
130
tests/components/hassio/snapshots/test_backup.ambr
Normal file
130
tests/components/hassio/snapshots/test_backup.ambr
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_config_load_config_info[storage_data0]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
]),
|
||||||
|
'include_addons': None,
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': None,
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[storage_data1]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent1',
|
||||||
|
'hassio.local',
|
||||||
|
'test-agent2',
|
||||||
|
]),
|
||||||
|
'include_addons': list([
|
||||||
|
'addon1',
|
||||||
|
'addon2',
|
||||||
|
]),
|
||||||
|
'include_all_addons': True,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': list([
|
||||||
|
'media',
|
||||||
|
'share',
|
||||||
|
]),
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_config_load_config_info[storage_data2]
|
||||||
|
dict({
|
||||||
|
'id': 1,
|
||||||
|
'result': dict({
|
||||||
|
'config': dict({
|
||||||
|
'agents': dict({
|
||||||
|
}),
|
||||||
|
'create_backup': dict({
|
||||||
|
'agent_ids': list([
|
||||||
|
'test-agent1',
|
||||||
|
'hassio.local',
|
||||||
|
'test-agent2',
|
||||||
|
]),
|
||||||
|
'include_addons': list([
|
||||||
|
'addon1',
|
||||||
|
'addon2',
|
||||||
|
]),
|
||||||
|
'include_all_addons': False,
|
||||||
|
'include_database': True,
|
||||||
|
'include_folders': list([
|
||||||
|
'media',
|
||||||
|
'share',
|
||||||
|
]),
|
||||||
|
'name': None,
|
||||||
|
'password': None,
|
||||||
|
}),
|
||||||
|
'last_attempted_automatic_backup': None,
|
||||||
|
'last_completed_automatic_backup': None,
|
||||||
|
'next_automatic_backup': None,
|
||||||
|
'next_automatic_backup_additional': False,
|
||||||
|
'retention': dict({
|
||||||
|
'copies': None,
|
||||||
|
'days': None,
|
||||||
|
}),
|
||||||
|
'schedule': dict({
|
||||||
|
'days': list([
|
||||||
|
]),
|
||||||
|
'recurrence': 'never',
|
||||||
|
'time': None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'success': True,
|
||||||
|
'type': 'result',
|
||||||
|
})
|
||||||
|
# ---
|
@ -30,6 +30,7 @@ from aiohasupervisor.models.backups import LOCATION_CLOUD_BACKUP, LOCATION_LOCAL
|
|||||||
from aiohasupervisor.models.mounts import MountsInfo
|
from aiohasupervisor.models.mounts import MountsInfo
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.backup import (
|
from homeassistant.components.backup import (
|
||||||
DOMAIN as BACKUP_DOMAIN,
|
DOMAIN as BACKUP_DOMAIN,
|
||||||
@ -38,6 +39,7 @@ from homeassistant.components.backup import (
|
|||||||
BackupAgent,
|
BackupAgent,
|
||||||
BackupAgentPlatformProtocol,
|
BackupAgentPlatformProtocol,
|
||||||
Folder,
|
Folder,
|
||||||
|
store as backup_store,
|
||||||
)
|
)
|
||||||
from homeassistant.components.hassio import DOMAIN
|
from homeassistant.components.hassio import DOMAIN
|
||||||
from homeassistant.components.hassio.backup import RESTORE_JOB_ID_ENV
|
from homeassistant.components.hassio.backup import RESTORE_JOB_ID_ENV
|
||||||
@ -2466,3 +2468,94 @@ async def test_restore_progress_after_restart_unknown_job(
|
|||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"]["last_non_idle_event"] is None
|
assert response["result"]["last_non_idle_event"] is None
|
||||||
assert response["result"]["state"] == "idle"
|
assert response["result"]["state"] == "idle"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"storage_data",
|
||||||
|
[
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"backup": {
|
||||||
|
"data": {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["test-agent1", "hassio.local", "test-agent2"],
|
||||||
|
"include_addons": ["addon1", "addon2"],
|
||||||
|
"include_all_addons": True,
|
||||||
|
"include_database": True,
|
||||||
|
"include_folders": ["media", "share"],
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"schedule": {
|
||||||
|
"days": [],
|
||||||
|
"recurrence": "never",
|
||||||
|
"state": "never",
|
||||||
|
"time": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": backup_store.STORAGE_VERSION,
|
||||||
|
"minor_version": backup_store.STORAGE_VERSION_MINOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"backup": {
|
||||||
|
"data": {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["test-agent1", "backup.local", "test-agent2"],
|
||||||
|
"include_addons": ["addon1", "addon2"],
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": True,
|
||||||
|
"include_folders": ["media", "share"],
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"schedule": {
|
||||||
|
"days": [],
|
||||||
|
"recurrence": "never",
|
||||||
|
"state": "never",
|
||||||
|
"time": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": backup_store.STORAGE_VERSION,
|
||||||
|
"minor_version": backup_store.STORAGE_VERSION_MINOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("hassio_client")
|
||||||
|
async def test_config_load_config_info(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
storage_data: dict[str, Any] | None,
|
||||||
|
) -> None:
|
||||||
|
"""Test loading stored backup config and reading it via config/info."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||||
|
freezer.move_to("2024-11-13T12:01:00+01:00")
|
||||||
|
|
||||||
|
hass_storage.update(storage_data)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
assert await client.receive_json() == snapshot
|
||||||
|
@ -539,6 +539,70 @@ async def test_list_exposed_entities(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_exposed_entities_with_filter(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test list exposed entities with filter."""
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
|
||||||
|
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
|
||||||
|
|
||||||
|
# Expose 1 to Alexa
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "homeassistant/expose_entity",
|
||||||
|
"assistants": ["cloud.alexa"],
|
||||||
|
"entity_ids": [entry1.entity_id],
|
||||||
|
"should_expose": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
# Expose 2 to Google
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "homeassistant/expose_entity",
|
||||||
|
"assistants": ["cloud.google_assistant"],
|
||||||
|
"entity_ids": [entry2.entity_id],
|
||||||
|
"should_expose": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
|
||||||
|
# List with filter
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{"type": "homeassistant/expose_entity/list", "assistant": "cloud.alexa"}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == {
|
||||||
|
"exposed_entities": {
|
||||||
|
"test.test_unique1": {"cloud.alexa": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "homeassistant/expose_entity/list",
|
||||||
|
"assistant": "cloud.google_assistant",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await ws_client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"] == {
|
||||||
|
"exposed_entities": {
|
||||||
|
"test.test_unique2": {"cloud.google_assistant": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_listeners(
|
async def test_listeners(
|
||||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -580,13 +580,19 @@ def alarm_clock_fixture_extended():
|
|||||||
return alarm_clock
|
return alarm_clock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="speaker_model")
|
||||||
|
def speaker_model_fixture(request: pytest.FixtureRequest):
|
||||||
|
"""Create fixture for the speaker model."""
|
||||||
|
return getattr(request, "param", "Model Name")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="speaker_info")
|
@pytest.fixture(name="speaker_info")
|
||||||
def speaker_info_fixture():
|
def speaker_info_fixture(speaker_model):
|
||||||
"""Create speaker_info fixture."""
|
"""Create speaker_info fixture."""
|
||||||
return {
|
return {
|
||||||
"zone_name": "Zone A",
|
"zone_name": "Zone A",
|
||||||
"uid": "RINCON_test",
|
"uid": "RINCON_test",
|
||||||
"model_name": "Model Name",
|
"model_name": speaker_model,
|
||||||
"model_number": "S12",
|
"model_number": "S12",
|
||||||
"hardware_version": "1.20.1.6-1.1",
|
"hardware_version": "1.20.1.6-1.1",
|
||||||
"software_version": "49.2-64250",
|
"software_version": "49.2-64250",
|
||||||
|
@ -10,6 +10,7 @@ from syrupy import SnapshotAssertion
|
|||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_INPUT_SOURCE,
|
ATTR_INPUT_SOURCE,
|
||||||
|
ATTR_INPUT_SOURCE_LIST,
|
||||||
ATTR_MEDIA_ANNOUNCE,
|
ATTR_MEDIA_ANNOUNCE,
|
||||||
ATTR_MEDIA_CONTENT_ID,
|
ATTR_MEDIA_CONTENT_ID,
|
||||||
ATTR_MEDIA_CONTENT_TYPE,
|
ATTR_MEDIA_CONTENT_TYPE,
|
||||||
@ -1205,3 +1206,27 @@ async def test_media_get_queue(
|
|||||||
)
|
)
|
||||||
soco_mock.get_queue.assert_called_with(max_items=0)
|
soco_mock.get_queue.assert_called_with(max_items=0)
|
||||||
assert result == snapshot
|
assert result == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("speaker_model", "source_list"),
|
||||||
|
[
|
||||||
|
("Sonos Arc Ultra", [SOURCE_TV]),
|
||||||
|
("Sonos Arc", [SOURCE_TV]),
|
||||||
|
("Sonos Playbar", [SOURCE_TV]),
|
||||||
|
("Sonos Connect", [SOURCE_LINEIN]),
|
||||||
|
("Sonos Play:5", [SOURCE_LINEIN]),
|
||||||
|
("Sonos Amp", [SOURCE_LINEIN, SOURCE_TV]),
|
||||||
|
("Sonos Era", None),
|
||||||
|
],
|
||||||
|
indirect=["speaker_model"],
|
||||||
|
)
|
||||||
|
async def test_media_source_list(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
async_autosetup_sonos,
|
||||||
|
speaker_model: str,
|
||||||
|
source_list: list[str] | None,
|
||||||
|
) -> None:
|
||||||
|
"""Test the mapping between the speaker model name and source_list."""
|
||||||
|
state = hass.states.get("media_player.zone_a")
|
||||||
|
assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == source_list
|
||||||
|
@ -34,7 +34,6 @@ from tests.common import (
|
|||||||
mock_integration,
|
mock_integration,
|
||||||
mock_platform,
|
mock_platform,
|
||||||
mock_restore_cache,
|
mock_restore_cache,
|
||||||
reset_translation_cache,
|
|
||||||
)
|
)
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
@ -519,9 +518,6 @@ async def test_default_engine_prefer_cloud_entity(
|
|||||||
assert provider_engine.name == "test"
|
assert provider_engine.name == "test"
|
||||||
assert async_default_engine(hass) == "stt.cloud_stt_entity"
|
assert async_default_engine(hass) == "stt.cloud_stt_entity"
|
||||||
|
|
||||||
# Reset the `cloud` translations cache to avoid flaky translation checks
|
|
||||||
reset_translation_cache(hass, ["cloud"])
|
|
||||||
|
|
||||||
|
|
||||||
async def test_get_engine_legacy(
|
async def test_get_engine_legacy(
|
||||||
hass: HomeAssistant, tmp_path: Path, mock_provider: MockSTTProvider
|
hass: HomeAssistant, tmp_path: Path, mock_provider: MockSTTProvider
|
||||||
|
@ -44,7 +44,6 @@ from tests.common import (
|
|||||||
mock_integration,
|
mock_integration,
|
||||||
mock_platform,
|
mock_platform,
|
||||||
mock_restore_cache,
|
mock_restore_cache,
|
||||||
reset_translation_cache,
|
|
||||||
)
|
)
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
@ -1987,6 +1986,3 @@ async def test_default_engine_prefer_cloud_entity(
|
|||||||
provider_engine = tts.async_resolve_engine(hass, "test")
|
provider_engine = tts.async_resolve_engine(hass, "test")
|
||||||
assert provider_engine == "test"
|
assert provider_engine == "test"
|
||||||
assert tts.async_default_engine(hass) == "tts.cloud_tts_entity"
|
assert tts.async_default_engine(hass) == "tts.cloud_tts_entity"
|
||||||
|
|
||||||
# Reset the `cloud` translations cache to avoid flaky translation checks
|
|
||||||
reset_translation_cache(hass, ["cloud"])
|
|
||||||
|
@ -171,6 +171,7 @@
|
|||||||
'models': list([
|
'models': list([
|
||||||
'LV-PUR131S',
|
'LV-PUR131S',
|
||||||
'LV-RH131S',
|
'LV-RH131S',
|
||||||
|
'LV-RH131S-WM',
|
||||||
]),
|
]),
|
||||||
'modes': list([
|
'modes': list([
|
||||||
'manual',
|
'manual',
|
||||||
|
@ -11,6 +11,7 @@ import gc
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import reprlib
|
import reprlib
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@ -49,7 +50,7 @@ from . import patch_recorder
|
|||||||
# Setup patching of dt_util time functions before any other Home Assistant imports
|
# Setup patching of dt_util time functions before any other Home Assistant imports
|
||||||
from . import patch_time # noqa: F401, isort:skip
|
from . import patch_time # noqa: F401, isort:skip
|
||||||
|
|
||||||
from homeassistant import core as ha, loader, runner
|
from homeassistant import components, core as ha, loader, runner
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||||
from homeassistant.auth.models import Credentials
|
from homeassistant.auth.models import Credentials
|
||||||
from homeassistant.auth.providers import homeassistant
|
from homeassistant.auth.providers import homeassistant
|
||||||
@ -85,6 +86,7 @@ from homeassistant.helpers import (
|
|||||||
issue_registry as ir,
|
issue_registry as ir,
|
||||||
label_registry as lr,
|
label_registry as lr,
|
||||||
recorder as recorder_helper,
|
recorder as recorder_helper,
|
||||||
|
translation as translation_helper,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.translation import _TranslationsCacheData
|
from homeassistant.helpers.translation import _TranslationsCacheData
|
||||||
@ -1211,9 +1213,8 @@ def mock_get_source_ip() -> Generator[_patch]:
|
|||||||
def translations_once() -> Generator[_patch]:
|
def translations_once() -> Generator[_patch]:
|
||||||
"""Only load translations once per session.
|
"""Only load translations once per session.
|
||||||
|
|
||||||
Warning: having this as a session fixture can cause issues with tests that
|
Note: To avoid issues with tests that mock integrations, translations for
|
||||||
create mock integrations, overriding the real integration translations
|
mocked integrations are cleaned up by the evict_faked_translations fixture.
|
||||||
with empty ones. Translations should be reset after such tests (see #131628)
|
|
||||||
"""
|
"""
|
||||||
cache = _TranslationsCacheData({}, {})
|
cache = _TranslationsCacheData({}, {})
|
||||||
patcher = patch(
|
patcher = patch(
|
||||||
@ -1227,6 +1228,30 @@ def translations_once() -> Generator[_patch]:
|
|||||||
patcher.stop()
|
patcher.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="module")
|
||||||
|
def evict_faked_translations(translations_once) -> Generator[_patch]:
|
||||||
|
"""Clear translations for mocked integrations from the cache after each module."""
|
||||||
|
real_component_strings = translation_helper._async_get_component_strings
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_get_component_strings",
|
||||||
|
wraps=real_component_strings,
|
||||||
|
) as mock_component_strings:
|
||||||
|
yield
|
||||||
|
cache: _TranslationsCacheData = translations_once.kwargs["return_value"]
|
||||||
|
component_paths = components.__path__
|
||||||
|
|
||||||
|
for call in mock_component_strings.mock_calls:
|
||||||
|
integrations: dict[str, loader.Integration] = call.args[3]
|
||||||
|
for domain, integration in integrations.items():
|
||||||
|
if any(
|
||||||
|
pathlib.Path(f"{component_path}/{domain}") == integration.file_path
|
||||||
|
for component_path in component_paths
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
for loaded_for_lang in cache.loaded.values():
|
||||||
|
loaded_for_lang.discard(domain)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def disable_translations_once(
|
def disable_translations_once(
|
||||||
translations_once: _patch,
|
translations_once: _patch,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Test test fixture configuration."""
|
"""Test test fixture configuration."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
import pathlib
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@ -9,8 +11,11 @@ import pytest_socket
|
|||||||
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.core import HomeAssistant, async_get_hass
|
from homeassistant.core import HomeAssistant, async_get_hass
|
||||||
|
from homeassistant.helpers import translation
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .common import MockModule, mock_integration
|
||||||
|
from .conftest import evict_faked_translations
|
||||||
from .typing import ClientSessionGenerator
|
from .typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
@ -70,3 +75,46 @@ async def test_aiohttp_client_frozen_router_view(
|
|||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
result = await response.json()
|
result = await response.json()
|
||||||
assert result["test"] is True
|
assert result["test"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_evict_faked_translations_assumptions(hass: HomeAssistant) -> None:
|
||||||
|
"""Test assumptions made when detecting translations for mocked integrations.
|
||||||
|
|
||||||
|
If this test fails, the evict_faked_translations may need to be updated.
|
||||||
|
"""
|
||||||
|
integration = mock_integration(hass, MockModule("test"), built_in=True)
|
||||||
|
assert integration.file_path == pathlib.Path("")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_evict_faked_translations(hass: HomeAssistant, translations_once) -> None:
|
||||||
|
"""Test the evict_faked_translations fixture."""
|
||||||
|
cache: translation._TranslationsCacheData = translations_once.kwargs["return_value"]
|
||||||
|
fake_domain = "test"
|
||||||
|
real_domain = "homeassistant"
|
||||||
|
|
||||||
|
# Evict the real domain from the cache in case it's been loaded before
|
||||||
|
cache.loaded["en"].discard(real_domain)
|
||||||
|
|
||||||
|
assert fake_domain not in cache.loaded["en"]
|
||||||
|
assert real_domain not in cache.loaded["en"]
|
||||||
|
|
||||||
|
# The evict_faked_translations fixture has module scope, so we set it up and
|
||||||
|
# tear it down manually
|
||||||
|
real_func = evict_faked_translations.__pytest_wrapped__.obj
|
||||||
|
gen: Generator = real_func(translations_once)
|
||||||
|
|
||||||
|
# Set up the evict_faked_translations fixture
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
mock_integration(hass, MockModule(fake_domain), built_in=True)
|
||||||
|
await translation.async_load_integrations(hass, {fake_domain, real_domain})
|
||||||
|
assert fake_domain in cache.loaded["en"]
|
||||||
|
assert real_domain in cache.loaded["en"]
|
||||||
|
|
||||||
|
# Tear down the evict_faked_translations fixture
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# The mock integration should be removed from the cache, the real domain should still be there
|
||||||
|
assert fake_domain not in cache.loaded["en"]
|
||||||
|
assert real_domain in cache.loaded["en"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user