Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2025.2.5 #139012

Merged
merged 30 commits into from
Feb 21, 2025
Merged

2025.2.5 #139012

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c9a0814
Adjust Tuya Water Detector to support 1 as an alarm state (#135933)
petacz Feb 20, 2025
417ac56
Fix bug in set_preset_mode_with_end_datetime (wrong typo of frost_gua…
pectum83 Feb 20, 2025
b40daf0
Bump pyhive-integration to 1.0.2 (#138569)
KJonline Feb 15, 2025
7b82781
Bump tesla-fleet-api to v0.9.10 (#138575)
Bre77 Feb 15, 2025
e60b648
Bump pysmarty2 to 0.10.2 (#138625)
lucab-91 Feb 16, 2025
1e49e04
Rename "returned" state to "alert" (#138676)
shaiu Feb 16, 2025
2b7543a
Bump pyvesync for vesync (#138681)
cdnninja Feb 17, 2025
179ba83
Opower: Fix unavailable "start date" and "end date" sensors (#138694)
SaswatPadhi Feb 20, 2025
66bb501
Correct backup filename on delete or download of cloud backup (#138704)
emontnemery Feb 17, 2025
35bcf82
Correct invalid automatic backup settings when loading from store (#1…
emontnemery Feb 19, 2025
167881e
Bump airgradient to 0.9.2 (#138725)
joostlek Feb 17, 2025
6070fee
Clean up translations for mocked integrations inbetween tests (#138732)
emontnemery Feb 19, 2025
ac21d28
Bump pyrympro from 0.0.8 to 0.0.9 (#138753)
nivstein Feb 18, 2025
59651c6
Don't allow setting backup retention to 0 days or copies (#138771)
emontnemery Feb 18, 2025
12e530d
Fix TV input source option for Sonos Arc Ultra (#138778)
PeteRager Feb 18, 2025
4419177
Add assistant filter to expose entities list command (#138817)
synesthesiam Feb 19, 2025
d42e31b
Fix playback for encrypted Reolink files (#138852)
starkillerOG Feb 19, 2025
6da33a8
Correct backup date when reading a backup created by supervisor (#138…
emontnemery Feb 19, 2025
94555f5
Bump pyfritzhome to 0.6.15 (#138879)
mib1185 Feb 19, 2025
8c3ee80
Validate hassio backup settings (#138880)
emontnemery Feb 20, 2025
d752a3a
Catch zeep fault as well on GetSystemDateAndTime call. (#138916)
DmitryKuzmenko Feb 20, 2025
dc7cba6
Fix Reolink callback id collision (#138918)
starkillerOG Feb 20, 2025
266612e
Fix handling of min/max temperature presets in AVM Fritz!SmartHome (#…
mib1185 Feb 20, 2025
83d9c00
Bump pyprosegur to 0.0.13 (#138960)
dgomes Feb 20, 2025
3ea1d28
Bump reolink-aio to 0.12.0 (#138985)
starkillerOG Feb 21, 2025
325022e
Bump deebot-client to 12.2.0 (#138986)
edenhaus Feb 21, 2025
0dbdb42
Omit unknown hue effects (#138992)
joostlek Feb 21, 2025
df5f6fc
Update frontend to 20250221.0 (#139006)
bramkragten Feb 21, 2025
ba1650b
Bump version to 2025.2.5
frenck Feb 21, 2025
3d2ab3b
Make backup config update a callback (#138925)
MartinHjelmare Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/airgradient/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.9.1"],
"requirements": ["airgradient==0.9.2"],
"zeroconf": ["_airgradient._tcp.local."]
}
3 changes: 3 additions & 0 deletions homeassistant/components/backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
BackupAgentPlatformProtocol,
LocalBackupAgent,
)
from .config import BackupConfig, CreateBackupParametersDict
from .const import DATA_MANAGER, DOMAIN
from .http import async_register_http_views
from .manager import (
Expand Down Expand Up @@ -47,12 +48,14 @@
"BackupAgent",
"BackupAgentError",
"BackupAgentPlatformProtocol",
"BackupConfig",
"BackupManagerError",
"BackupNotFound",
"BackupPlatformProtocol",
"BackupReaderWriter",
"BackupReaderWriterError",
"CreateBackupEvent",
"CreateBackupParametersDict",
"CreateBackupStage",
"CreateBackupState",
"Folder",
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/backup/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ def load(self, stored_config: StoredBackupConfig) -> None:
self.data.retention.apply(self._manager)
self.data.schedule.apply(self._manager)

async def update(
@callback
def update(
self,
*,
agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED,
Expand Down
49 changes: 48 additions & 1 deletion homeassistant/components/backup/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
BackupAgentPlatformProtocol,
LocalBackupAgent,
)
from .config import BackupConfig, delete_backups_exceeding_configured_count
from .config import (
BackupConfig,
CreateBackupParametersDict,
delete_backups_exceeding_configured_count,
)
from .const import (
BUF_SIZE,
DATA_MANAGER,
Expand Down Expand Up @@ -282,6 +286,10 @@ async def async_resume_restore_progress_after_restart(
) -> None:
"""Get restore events after core restart."""

@abc.abstractmethod
async def async_validate_config(self, *, config: BackupConfig) -> None:
"""Validate backup config."""


class IncorrectPasswordError(BackupReaderWriterError):
"""Raised when the password is incorrect."""
Expand Down Expand Up @@ -333,6 +341,7 @@ async def async_setup(self) -> None:
self.config.load(stored["config"])
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(
on_progress=self.async_on_backup_event
)
Expand Down Expand Up @@ -1832,6 +1841,44 @@ def _read_restore_file() -> json_util.JsonObjectType | None:
)
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:
"""Generate a backup ID."""
Expand Down
9 changes: 8 additions & 1 deletion homeassistant/components/backup/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
STORE_DELAY_SAVE = 30
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 3
STORAGE_VERSION_MINOR = 4


class StoredBackupData(TypedDict):
Expand Down Expand Up @@ -60,6 +60,13 @@ async def _async_migrate_func(
else:
data["config"]["schedule"]["days"] = [state]
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.
# Reject if major version is higher than 2.
Expand Down
7 changes: 5 additions & 2 deletions homeassistant/components/backup/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,15 @@ def read_backup(backup_path: Path) -> AgentBackup:
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(
addons=addons,
backup_id=cast(str, data["slug"]),
database_included=database_included,
date=cast(str, data["date"]),
extra_metadata=cast(dict[str, bool | str], data.get("extra", {})),
date=cast(str, date),
extra_metadata=extra_metadata,
folders=folders,
homeassistant_included=homeassistant_included,
homeassistant_version=homeassistant_version,
Expand Down
12 changes: 7 additions & 5 deletions homeassistant/components/backup/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ async def handle_config_info(
)


@callback
@websocket_api.require_admin
@websocket_api.websocket_command(
{
Expand All @@ -368,8 +369,10 @@ async def handle_config_info(
),
vol.Optional("retention"): vol.Schema(
{
vol.Optional("copies"): vol.Any(int, None),
vol.Optional("days"): vol.Any(int, None),
# Note: We can't use cv.positive_int because it allows 0 even
# 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(
Expand All @@ -385,8 +388,7 @@ async def handle_config_info(
),
}
)
@websocket_api.async_response
async def handle_config_update(
def handle_config_update(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
Expand All @@ -396,7 +398,7 @@ async def handle_config_update(
changes = dict(msg)
changes.pop("id")
changes.pop("type")
await manager.config.update(**changes)
manager.config.update(**changes)
connection.send_result(msg["id"])


Expand Down
43 changes: 28 additions & 15 deletions homeassistant/components/cloud/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from aiohttp import ClientError
from hass_nabucasa import Cloud, CloudError
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 homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
Expand Down Expand Up @@ -76,11 +80,6 @@ def __init__(self, hass: HomeAssistant, cloud: Cloud[CloudClient]) -> None:
self._cloud = cloud
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(
self,
backup_id: str,
Expand All @@ -91,13 +90,13 @@ async def async_download_backup(
:param backup_id: The ID of the backup that was returned in async_list_backups.
: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")

try:
content = await self._cloud.files.download(
storage_type=StorageType.BACKUP,
filename=self._get_backup_filename(),
filename=backup["Key"],
)
except CloudError as err:
raise BackupAgentError(f"Failed to download backup: {err}") from err
Expand All @@ -124,7 +123,7 @@ async def async_upload_backup(
base64md5hash = await calculate_b64md5(open_stream, size)
except FilesError as err:
raise BackupAgentError(err) from err
filename = self._get_backup_filename()
filename = f"{self._cloud.client.prefs.instance_id}.tar"
metadata = backup.as_dict()

tries = 1
Expand Down Expand Up @@ -172,40 +171,54 @@ async def async_delete_backup(
: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

try:
await async_files_delete_file(
self._cloud,
storage_type=StorageType.BACKUP,
filename=self._get_backup_filename(),
filename=backup["Key"],
)
except (ClientError, CloudError) as err:
raise BackupAgentError("Failed to delete backup") from err

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."""
try:
backups = await async_files_list(
self._cloud, storage_type=StorageType.BACKUP
)
_LOGGER.debug("Cloud backups: %s", backups)
except (ClientError, CloudError) as 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(
self,
backup_id: str,
**kwargs: Any,
) -> AgentBackup | None:
"""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:
if backup.backup_id == backup_id:
if backup["Metadata"]["backup_id"] == backup_id:
return backup

return None
2 changes: 1 addition & 1 deletion homeassistant/components/ecovacs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"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"]
}
26 changes: 10 additions & 16 deletions homeassistant/components/fritzbox/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def _add_entities(devices: set[str] | None = None) -> None:
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
"""The thermostat class for FRITZ!SmartHome thermostats."""

_attr_max_temp = MAX_TEMPERATURE
_attr_min_temp = MIN_TEMPERATURE
_attr_precision = PRECISION_HALVES
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "thermostat"
Expand Down Expand Up @@ -135,11 +137,13 @@ def target_temperature(self) -> float:

async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode == HVACMode.OFF:
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
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(
self.data.set_target_temperature, target_temp, True
)
Expand Down Expand Up @@ -169,12 +173,12 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
translation_domain=DOMAIN,
translation_key="change_hvac_while_active_mode",
)
if self.hvac_mode == hvac_mode:
if self.hvac_mode is hvac_mode:
LOGGER.debug(
"%s is already in requested hvac mode %s", self.name, hvac_mode
)
return
if hvac_mode == HVACMode.OFF:
if hvac_mode is HVACMode.OFF:
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
else:
if value_scheduled_preset(self.data) == PRESET_ECO:
Expand Down Expand Up @@ -208,16 +212,6 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
elif preset_mode == PRESET_ECO:
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
def extra_state_attributes(self) -> ClimateExtraAttributes:
"""Return the device specific state attributes."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/fritzbox/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyfritzhome"],
"requirements": ["pyfritzhome==0.6.14"],
"requirements": ["pyfritzhome==0.6.15"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250214.0"]
"requirements": ["home-assistant-frontend==20250221.0"]
}
Loading
Loading