Skip to content

Commit 00627b8

Browse files
authored
2025.5.1 (#144564)
2 parents e8bdc72 + 13aba62 commit 00627b8

35 files changed

+430
-132
lines changed

homeassistant/components/backup/http.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .agent import BackupAgent
2323
from .const import DATA_MANAGER
2424
from .manager import BackupManager
25-
from .models import BackupNotFound
25+
from .models import AgentBackup, BackupNotFound
2626

2727

2828
@callback
@@ -85,7 +85,15 @@ async def get(
8585
request, headers, backup_id, agent_id, agent, manager
8686
)
8787
return await self._send_backup_with_password(
88-
hass, request, headers, backup_id, agent_id, password, agent, manager
88+
hass,
89+
backup,
90+
request,
91+
headers,
92+
backup_id,
93+
agent_id,
94+
password,
95+
agent,
96+
manager,
8997
)
9098
except BackupNotFound:
9199
return Response(status=HTTPStatus.NOT_FOUND)
@@ -116,6 +124,7 @@ async def _send_backup_no_password(
116124
async def _send_backup_with_password(
117125
self,
118126
hass: HomeAssistant,
127+
backup: AgentBackup,
119128
request: Request,
120129
headers: dict[istr, str],
121130
backup_id: str,
@@ -144,7 +153,8 @@ def on_done(error: Exception | None) -> None:
144153

145154
stream = util.AsyncIteratorWriter(hass)
146155
worker = threading.Thread(
147-
target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []]
156+
target=util.decrypt_backup,
157+
args=[backup, reader, stream, password, on_done, 0, []],
148158
)
149159
try:
150160
worker.start()

homeassistant/components/backup/util.py

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,26 @@ def validate_password_stream(
295295
raise BackupEmpty
296296

297297

298+
def _get_expected_archives(backup: AgentBackup) -> set[str]:
299+
"""Get the expected archives in the backup."""
300+
expected_archives = set()
301+
if backup.homeassistant_included:
302+
expected_archives.add("homeassistant")
303+
for addon in backup.addons:
304+
expected_archives.add(addon.slug)
305+
for folder in backup.folders:
306+
expected_archives.add(folder.value)
307+
return expected_archives
308+
309+
298310
def decrypt_backup(
311+
backup: AgentBackup,
299312
input_stream: IO[bytes],
300313
output_stream: IO[bytes],
301314
password: str | None,
302315
on_done: Callable[[Exception | None], None],
303316
minimum_size: int,
304-
nonces: list[bytes],
317+
nonces: NonceGenerator,
305318
) -> None:
306319
"""Decrypt a backup."""
307320
error: Exception | None = None
@@ -315,7 +328,7 @@ def decrypt_backup(
315328
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
316329
) as output_tar,
317330
):
318-
_decrypt_backup(input_tar, output_tar, password)
331+
_decrypt_backup(backup, input_tar, output_tar, password)
319332
except (DecryptError, SecureTarError, tarfile.TarError) as err:
320333
LOGGER.warning("Error decrypting backup: %s", err)
321334
error = err
@@ -333,15 +346,18 @@ def decrypt_backup(
333346

334347

335348
def _decrypt_backup(
349+
backup: AgentBackup,
336350
input_tar: tarfile.TarFile,
337351
output_tar: tarfile.TarFile,
338352
password: str | None,
339353
) -> None:
340354
"""Decrypt a backup."""
355+
expected_archives = _get_expected_archives(backup)
341356
for obj in input_tar:
342357
# We compare with PurePath to avoid issues with different path separators,
343358
# for example when backup.json is added as "./backup.json"
344-
if PurePath(obj.name) == PurePath("backup.json"):
359+
object_path = PurePath(obj.name)
360+
if object_path == PurePath("backup.json"):
345361
# Rewrite the backup.json file to indicate that the backup is decrypted
346362
if not (reader := input_tar.extractfile(obj)):
347363
raise DecryptError
@@ -352,7 +368,13 @@ def _decrypt_backup(
352368
metadata_obj.size = len(updated_metadata_b)
353369
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
354370
continue
355-
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
371+
prefix, _, suffix = object_path.name.partition(".")
372+
if suffix not in ("tar", "tgz", "tar.gz"):
373+
LOGGER.debug("Unknown file %s will not be decrypted", obj.name)
374+
output_tar.addfile(obj, input_tar.extractfile(obj))
375+
continue
376+
if prefix not in expected_archives:
377+
LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name)
356378
output_tar.addfile(obj, input_tar.extractfile(obj))
357379
continue
358380
istf = SecureTarFile(
@@ -371,12 +393,13 @@ def _decrypt_backup(
371393

372394

373395
def encrypt_backup(
396+
backup: AgentBackup,
374397
input_stream: IO[bytes],
375398
output_stream: IO[bytes],
376399
password: str | None,
377400
on_done: Callable[[Exception | None], None],
378401
minimum_size: int,
379-
nonces: list[bytes],
402+
nonces: NonceGenerator,
380403
) -> None:
381404
"""Encrypt a backup."""
382405
error: Exception | None = None
@@ -390,7 +413,7 @@ def encrypt_backup(
390413
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
391414
) as output_tar,
392415
):
393-
_encrypt_backup(input_tar, output_tar, password, nonces)
416+
_encrypt_backup(backup, input_tar, output_tar, password, nonces)
394417
except (EncryptError, SecureTarError, tarfile.TarError) as err:
395418
LOGGER.warning("Error encrypting backup: %s", err)
396419
error = err
@@ -408,17 +431,20 @@ def encrypt_backup(
408431

409432

410433
def _encrypt_backup(
434+
backup: AgentBackup,
411435
input_tar: tarfile.TarFile,
412436
output_tar: tarfile.TarFile,
413437
password: str | None,
414-
nonces: list[bytes],
438+
nonces: NonceGenerator,
415439
) -> None:
416440
"""Encrypt a backup."""
417441
inner_tar_idx = 0
442+
expected_archives = _get_expected_archives(backup)
418443
for obj in input_tar:
419444
# We compare with PurePath to avoid issues with different path separators,
420445
# for example when backup.json is added as "./backup.json"
421-
if PurePath(obj.name) == PurePath("backup.json"):
446+
object_path = PurePath(obj.name)
447+
if object_path == PurePath("backup.json"):
422448
# Rewrite the backup.json file to indicate that the backup is encrypted
423449
if not (reader := input_tar.extractfile(obj)):
424450
raise EncryptError
@@ -429,16 +455,21 @@ def _encrypt_backup(
429455
metadata_obj.size = len(updated_metadata_b)
430456
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
431457
continue
432-
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
458+
prefix, _, suffix = object_path.name.partition(".")
459+
if suffix not in ("tar", "tgz", "tar.gz"):
460+
LOGGER.debug("Unknown file %s will not be encrypted", obj.name)
433461
output_tar.addfile(obj, input_tar.extractfile(obj))
434462
continue
463+
if prefix not in expected_archives:
464+
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
465+
continue
435466
istf = SecureTarFile(
436467
None, # Not used
437468
gzip=False,
438469
key=password_to_key(password) if password is not None else None,
439470
mode="r",
440471
fileobj=input_tar.extractfile(obj),
441-
nonce=nonces[inner_tar_idx],
472+
nonce=nonces.get(inner_tar_idx),
442473
)
443474
inner_tar_idx += 1
444475
with istf.encrypt(obj) as encrypted:
@@ -456,17 +487,33 @@ class _CipherWorkerStatus:
456487
writer: AsyncIteratorWriter
457488

458489

490+
class NonceGenerator:
491+
"""Generate nonces for encryption."""
492+
493+
def __init__(self) -> None:
494+
"""Initialize the generator."""
495+
self._nonces: dict[int, bytes] = {}
496+
497+
def get(self, index: int) -> bytes:
498+
"""Get a nonce for the given index."""
499+
if index not in self._nonces:
500+
# Generate a new nonce for the given index
501+
self._nonces[index] = os.urandom(16)
502+
return self._nonces[index]
503+
504+
459505
class _CipherBackupStreamer:
460506
"""Encrypt or decrypt a backup."""
461507

462508
_cipher_func: Callable[
463509
[
510+
AgentBackup,
464511
IO[bytes],
465512
IO[bytes],
466513
str | None,
467514
Callable[[Exception | None], None],
468515
int,
469-
list[bytes],
516+
NonceGenerator,
470517
],
471518
None,
472519
]
@@ -484,7 +531,7 @@ def __init__(
484531
self._hass = hass
485532
self._open_stream = open_stream
486533
self._password = password
487-
self._nonces: list[bytes] = []
534+
self._nonces = NonceGenerator()
488535

489536
def size(self) -> int:
490537
"""Return the maximum size of the decrypted or encrypted backup."""
@@ -508,7 +555,15 @@ def on_done(error: Exception | None) -> None:
508555
writer = AsyncIteratorWriter(self._hass)
509556
worker = threading.Thread(
510557
target=self._cipher_func,
511-
args=[reader, writer, self._password, on_done, self.size(), self._nonces],
558+
args=[
559+
self._backup,
560+
reader,
561+
writer,
562+
self._password,
563+
on_done,
564+
self.size(),
565+
self._nonces,
566+
],
512567
)
513568
worker_status = _CipherWorkerStatus(
514569
done=asyncio.Event(), reader=reader, thread=worker, writer=writer
@@ -538,17 +593,6 @@ def backup(self) -> AgentBackup:
538593
class EncryptedBackupStreamer(_CipherBackupStreamer):
539594
"""Encrypt a backup."""
540595

541-
def __init__(
542-
self,
543-
hass: HomeAssistant,
544-
backup: AgentBackup,
545-
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
546-
password: str | None,
547-
) -> None:
548-
"""Initialize."""
549-
super().__init__(hass, backup, open_stream, password)
550-
self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())]
551-
552596
_cipher_func = staticmethod(encrypt_backup)
553597

554598
def backup(self) -> AgentBackup:

homeassistant/components/dnsip/config_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def async_check(
6868
result = False
6969
with contextlib.suppress(DNSError):
7070
result = bool(
71-
await aiodns.DNSResolver(
71+
await aiodns.DNSResolver( # type: ignore[call-overload]
7272
nameservers=[resolver], udp_port=port, tcp_port=port
7373
).query(hostname, qtype)
7474
)

homeassistant/components/dnsip/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"config_flow": true,
66
"documentation": "https://www.home-assistant.io/integrations/dnsip",
77
"iot_class": "cloud_polling",
8-
"requirements": ["aiodns==3.3.0"]
8+
"requirements": ["aiodns==3.4.0"]
99
}

homeassistant/components/dnsip/sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def __init__(
106106
async def async_update(self) -> None:
107107
"""Get the current DNS IP address for hostname."""
108108
try:
109-
response = await self.resolver.query(self.hostname, self.querytype)
109+
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
110110
except DNSError as err:
111111
_LOGGER.warning("Exception while resolving host: %s", err)
112112
response = None

homeassistant/components/forecast_solar/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
77
"integration_type": "service",
88
"iot_class": "cloud_polling",
9-
"requirements": ["forecast-solar==4.1.0"]
9+
"requirements": ["forecast-solar==4.2.0"]
1010
}

homeassistant/components/fritzbox/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def cleanup_removed_devices(self, data: FritzboxCoordinatorData) -> None:
9292

9393
available_main_ains = [
9494
ain
95-
for ain, dev in data.devices.items()
95+
for ain, dev in data.devices.items() | data.templates.items()
9696
if dev.device_and_unit_id[1] is None
9797
]
9898
device_reg = dr.async_get(self.hass)

homeassistant/components/fronius/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,15 @@
4545
async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
4646
"""Set up fronius from a config entry."""
4747
host = entry.data[CONF_HOST]
48-
fronius = Fronius(async_get_clientsession(hass), host)
48+
fronius = Fronius(
49+
async_get_clientsession(
50+
hass,
51+
# Fronius Gen24 firmware 1.35.4-1 redirects to HTTPS with self-signed
52+
# certificate. See https://github.com/home-assistant/core/issues/138881
53+
verify_ssl=False,
54+
),
55+
host,
56+
)
4957
solar_net = FroniusSolarNet(hass, entry, fronius)
5058
await solar_net.init_devices()
5159

homeassistant/components/frontend/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@
2020
"documentation": "https://www.home-assistant.io/integrations/frontend",
2121
"integration_type": "system",
2222
"quality_scale": "internal",
23-
"requirements": ["home-assistant-frontend==20250507.0"]
23+
"requirements": ["home-assistant-frontend==20250509.0"]
2424
}

homeassistant/components/homekit/type_air_purifiers.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from pyhap.service import Service
99
from pyhap.util import callback as pyhap_callback
1010

11-
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
11+
from homeassistant.const import (
12+
ATTR_UNIT_OF_MEASUREMENT,
13+
STATE_ON,
14+
STATE_UNAVAILABLE,
15+
STATE_UNKNOWN,
16+
UnitOfTemperature,
17+
)
1218
from homeassistant.core import (
1319
Event,
1420
EventStateChangedData,
@@ -43,7 +49,12 @@
4349
THRESHOLD_FILTER_CHANGE_NEEDED,
4450
)
4551
from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan
46-
from .util import cleanup_name_for_homekit, convert_to_float, density_to_air_quality
52+
from .util import (
53+
cleanup_name_for_homekit,
54+
convert_to_float,
55+
density_to_air_quality,
56+
temperature_to_homekit,
57+
)
4758

4859
_LOGGER = logging.getLogger(__name__)
4960

@@ -345,8 +356,13 @@ def _async_update_current_temperature(self, new_state: State | None) -> None:
345356
):
346357
return
347358

359+
unit = new_state.attributes.get(
360+
ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS
361+
)
362+
current_temperature = temperature_to_homekit(current_temperature, unit)
363+
348364
_LOGGER.debug(
349-
"%s: Linked temperature sensor %s changed to %d",
365+
"%s: Linked temperature sensor %s changed to %d °C",
350366
self.entity_id,
351367
self.linked_temperature_sensor,
352368
current_temperature,

homeassistant/components/lamarzocco/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@
3737
"iot_class": "cloud_push",
3838
"loggers": ["pylamarzocco"],
3939
"quality_scale": "platinum",
40-
"requirements": ["pylamarzocco==2.0.0"]
40+
"requirements": ["pylamarzocco==2.0.1"]
4141
}

0 commit comments

Comments
 (0)