Skip to content
Merged

2026.4.2 #167939

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
7eda592
Improve handling of disconnected meters with Rainforest Automation Ea…
SkySrfr Apr 4, 2026
ab601e5
Prevent the intellifire client from polling independently of its coor…
jeeftor Apr 9, 2026
b880876
Switchbot Cloud: Enable Webhook for Bot (#165647)
XiaoLing-git Apr 9, 2026
850b034
Include port in BSB-LAN configuration URL when non-default (#166480)
liudger Apr 6, 2026
fb1365e
[LG Soundbar] Fix incorrect state for some models (#167094)
alexmerkel Apr 10, 2026
98a4e27
Bump aiopvpc to 4.3.1 (#167189)
marcomsousa Apr 5, 2026
c32d523
Bump starlink-grpc-core to 1.2.5 (#167195)
patcfly Apr 5, 2026
ed0b68e
Allow force alarm actions for Comelit (#167202)
chemelli74 Apr 9, 2026
dbfde92
Add Hisense AC (0x138C/0x0101) to Matter dry and fan mode device list…
lboue Apr 3, 2026
d644348
Bump pyTibber to 0.37.0 (#167283)
Danielhiversen Apr 6, 2026
6a934b5
Fix victron ble reauth flow title (#167307)
rajlaud Apr 3, 2026
6d3a93d
Update to tplink-omada-client 1.5.7 (#167313)
MarkGodwin Apr 3, 2026
c7bd673
Bump afsapi to 0.3.1 (#167321)
007hacky007 Apr 4, 2026
5c7c0a6
Bump pylutron to 0.4.1 (#167324)
cdheiser Apr 6, 2026
e854301
Bump cryptography to 46.0.6 (#167330)
pantherale0 Apr 5, 2026
040192c
Align and cleanup tests data for Fritz (#167363)
chemelli74 Apr 5, 2026
5f2fe4f
Bump aiohue to 4.8.1 (#167369)
joostlek Apr 4, 2026
586d7ab
Improve ProxmoxVE permissions handling (#167370)
CoMPaTech Apr 6, 2026
96a9b89
Bump axis to v68 to improve MQTT event resiliance (#167373)
Kane610 Apr 6, 2026
7188a09
Use dedicated session for seventeentrack to preserve login cookies (#…
shaiu Apr 6, 2026
7458605
Bump aiocomelit to 2.0.2 (#167414)
chemelli74 Apr 5, 2026
3333b8d
Fix setup without dhw (#167423)
liudger Apr 6, 2026
b5842b8
Fix handling of missing period statistics in Anglian Water coordinato…
pantherale0 Apr 6, 2026
3493517
Fix missing color_mode initialization in MQTT JSON light schema (#167…
noerstad Apr 7, 2026
f57e682
Bump jvcprojector dependency to pyjvcprojector 2.0.5 (#167450)
SteveEasley Apr 6, 2026
a892b53
Fix nzbget positional argument mismatch in NZBGetAPI calls (#167456)
JamieMagee Apr 6, 2026
6f4aca4
Update roborock services to raise ServiceNotSupported for new devices…
allenporter Apr 6, 2026
b028e2a
Miele - fix core temperature reading (#167476)
aturri Apr 6, 2026
ca9945f
Bump b2sdk to 2.10.4 (#167481)
ElCruncharino Apr 6, 2026
e5ff7a9
Handle BadRequest exception in Backblaze B2 config flow and setup (#1…
ElCruncharino Apr 6, 2026
b4f6a43
Bump pynintendoparental to 2.3.4 (#167510)
pantherale0 Apr 6, 2026
39fbdad
Add missing Miele dishwasher program ID 201 (#167536)
runningcode Apr 7, 2026
dc65646
Bump python-picnic-api2 to 1.3.4 (#167539)
xZise Apr 7, 2026
4a13ab9
Bump incomfort-client to v0.7.0 (#167546)
jbouwh Apr 6, 2026
0ce98cf
Remove homeassistant/actions/helpers/info from builder workflow (#167…
sairon Apr 7, 2026
c56d67c
Set up condition and trigger helpers in check config script (#167589)
arturpragacz Apr 7, 2026
a8cc099
fix EWS deviceType problem (#167597)
l-pr Apr 7, 2026
1aca993
Fix Tractive switch availability (#167599)
bieniu Apr 8, 2026
4c34dcd
Bump securetar to 2026.4.0 (#167600)
emontnemery Apr 7, 2026
a422611
Fix securetar size calculation when encrypting backup (#167602)
emontnemery Apr 7, 2026
f79285f
Bump holidays to 0.94 (#167604)
gjohansson-ST Apr 7, 2026
394670e
Fix ProxmoxVE migration causing reauthentication (#167624)
CoMPaTech Apr 8, 2026
fb766d1
Improve error logging for Backblaze B2 upload failures (#167721)
ElCruncharino Apr 10, 2026
ce755f5
Bump pyvlx to 0.2.33 (#167764)
wollew Apr 9, 2026
500f030
Set proper state for the internet_access switches in FRITZ!Box Tools …
mib1185 Apr 9, 2026
bd904ca
Bump aiotractive to 1.0.2 (#167783)
bieniu Apr 9, 2026
83da18b
Revert "Fix Ring snapshots" - #164337 (#167790)
bhudgens Apr 9, 2026
818bde1
Fix Victron BLE false reauth triggered by unknown enum bitmask combin…
rajlaud Apr 9, 2026
887e146
Fix Victron BLE storage errors caused by non-serializable value_fn ca…
rajlaud Apr 10, 2026
84490ef
Support Chess.com accounts with no name (#167824)
joostlek Apr 10, 2026
78107c4
Fix stale devices removal for Alexa devices (#167837)
chemelli74 Apr 10, 2026
ae5bd63
Fix service.yaml values for Home Connect (#167847)
Diegorro98 Apr 9, 2026
afcc211
Bump ZHA to 1.1.2 (#167849)
TheJulianJES Apr 10, 2026
d153eee
Bump velbusaio to 2026.4.0 (#167868)
cereal2nd Apr 10, 2026
0b5f85b
Bump zinvolt to 0.4.3 (#167908)
joostlek Apr 10, 2026
6bcfc32
Bump qbusmqttapi to 1.4.3 (#167909)
thomasddn Apr 10, 2026
1ae9e7c
Bump oasatelematics to 0.4 (#167911)
panosmz Apr 10, 2026
7d6eaf4
Fix light on action for qbus integration (#167917)
thomasddn Apr 10, 2026
a331cb7
Bump pylitterbot to 2025.2.1 (#167921)
natekspencer Apr 10, 2026
624fab0
Update frontend to 20260325.7 (#167922)
bramkragten Apr 10, 2026
a948799
Bump pyrisco to 0.6.8 (#167924)
OnFreund Apr 10, 2026
05463cd
Bump version to 2026.4.2
frenck Apr 10, 2026
8f2cec2
Merge branch 'master' into rc
frenck Apr 10, 2026
fcd6c6e
Improve Tibber price coordinator (#166175)
Danielhiversen Apr 10, 2026
fde103c
Fix tibber price sensor first state update (#167938)
MartinHjelmare Apr 10, 2026
cdce98f
Update cryptography to 46.0.7 (#167960)
frenck Apr 11, 2026
f2df848
Fix spelling of "Shut down" button label in `proxmoxve` (#167059)
NoRi2909 Apr 1, 2026
e4e9c22
Bump opower to 0.18.1 (#167967)
tronikos Apr 11, 2026
f7c5a51
Portainer fix fetching swarm stacks (#167979)
erwindouna Apr 11, 2026
190ee49
Bump python-bsblan to version 5.1.4 (#167987)
liudger Apr 11, 2026
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
11 changes: 10 additions & 1 deletion homeassistant/components/alexa_devices/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ def __init__(
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
)
self.previous_devices: set[str] = set()
device_registry = dr.async_get(hass)
self.previous_devices: set[str] = {
identifier
for device in device_registry.devices.get_devices_for_config_entry_id(
entry.entry_id
)
if device.entry_type != dr.DeviceEntryType.SERVICE
for identifier_domain, identifier in device.identifiers
if identifier_domain == DOMAIN
}

async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
Expand Down
39 changes: 30 additions & 9 deletions homeassistant/components/anglian_water/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ async def _insert_statistics(self) -> None:
_LOGGER.debug("Updating statistics for the first time")
usage_sum = 0.0
last_stats_time = None
allow_update_last_stored_hour = False
else:
if not meter.readings or len(meter.readings) == 0:
_LOGGER.debug("No recent usage statistics found, skipping update")
Expand All @@ -107,6 +108,7 @@ async def _insert_statistics(self) -> None:
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
_LOGGER.debug("Getting statistics at %s", start)
stats: dict[str, list[Any]] = {}
for end in (start + timedelta(seconds=1), None):
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
Expand All @@ -127,15 +129,28 @@ async def _insert_statistics(self) -> None:
"Not found, trying to find oldest statistic after %s",
start,
)
assert stats

def _safe_get_sum(records: list[Any]) -> float:
if records and "sum" in records[0]:
return float(records[0]["sum"])
return 0.0

usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
last_stats_time = stats[usage_statistic_id][0]["start"]
if not stats or not stats.get(usage_statistic_id):
_LOGGER.debug(
"Could not find existing statistics during period lookup for %s, "
"falling back to last stored statistic",
usage_statistic_id,
)
allow_update_last_stored_hour = True
last_records = last_stat[usage_statistic_id]
usage_sum = float(last_records[0].get("sum") or 0.0)
last_stats_time = last_records[0]["start"]
else:
allow_update_last_stored_hour = False
records = stats[usage_statistic_id]

def _safe_get_sum(records: list[Any]) -> float:
if records and "sum" in records[0]:
return float(records[0]["sum"])
return 0.0

usage_sum = _safe_get_sum(records)
last_stats_time = records[0]["start"]

usage_statistics = []

Expand All @@ -148,7 +163,13 @@ def _safe_get_sum(records: list[Any]) -> float:
)
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
if last_stats_time is not None and start.timestamp() <= last_stats_time:
if last_stats_time is not None and (
start.timestamp() < last_stats_time
or (
start.timestamp() == last_stats_time
and not allow_update_last_stored_hour
)
):
continue
usage_state = max(0, read["consumption"] / 1000)
usage_sum = max(0, read["read"])
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/axis/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==67"],
"requirements": ["axis==68"],
"ssdp": [
{
"manufacturer": "AXIS"
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/backblaze_b2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ def _authorize_and_get_bucket_sync() -> Bucket:
translation_domain=DOMAIN,
translation_key="invalid_bucket_name",
) from err
except exception.BadRequest as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="bad_request",
translation_placeholders={"error_message": str(err)},
) from err
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,
Expand Down
31 changes: 14 additions & 17 deletions homeassistant/components/backblaze_b2/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> T:
try:
return await func(*args, **kwargs)
except B2Error as err:
error_msg = f"Failed during {func.__name__}"
raise BackupAgentError(error_msg) from err
raise BackupAgentError(f"Failed during {func.__name__}: {err}") from err

return wrapper

Expand Down Expand Up @@ -170,8 +169,7 @@ def _is_cache_valid(self, expiration_time: float) -> bool:
async def _cleanup_failed_upload(self, filename: str) -> None:
"""Clean up a partially uploaded file after upload failure."""
_LOGGER.warning(
"Attempting to delete partially uploaded main backup file %s "
"due to metadata upload failure",
"Attempting to delete partially uploaded backup file %s",
filename,
)
try:
Expand All @@ -180,11 +178,10 @@ async def _cleanup_failed_upload(self, filename: str) -> None:
)
await self._hass.async_add_executor_job(uploaded_main_file_info.delete)
except B2Error:
_LOGGER.debug(
"Failed to clean up partially uploaded main backup file %s. "
"Manual intervention may be required to delete it from Backblaze B2",
_LOGGER.warning(
"Failed to clean up partially uploaded backup file %s;"
" manual deletion from Backblaze B2 may be required",
filename,
exc_info=True,
)
else:
_LOGGER.debug(
Expand Down Expand Up @@ -256,9 +253,10 @@ async def async_upload_backup(
prefixed_metadata_filename,
)

upload_successful = False
tar_uploaded = False
try:
await self._upload_backup_file(prefixed_tar_filename, open_stream, {})
tar_uploaded = True
_LOGGER.debug(
"Main backup file upload finished for %s", prefixed_tar_filename
)
Expand All @@ -270,15 +268,14 @@ async def async_upload_backup(
_LOGGER.debug(
"Metadata file upload finished for %s", prefixed_metadata_filename
)
upload_successful = True
finally:
if upload_successful:
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
self._invalidate_caches(
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
)
else:
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
self._invalidate_caches(
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
)
except B2Error:
if tar_uploaded:
await self._cleanup_failed_upload(prefixed_tar_filename)
raise

def _upload_metadata_file_sync(
self, metadata_content: bytes, filename: str
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/backblaze_b2/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ def _authorize_and_get_bucket_sync() -> None:
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
)
errors[CONF_BUCKET] = "invalid_bucket_name"
except exception.BadRequest as err:
_LOGGER.error(
"Backblaze B2 API rejected the request for Key ID '%s': %s",
user_input[CONF_KEY_ID],
err,
)
errors["base"] = "bad_request"
placeholders["error_message"] = str(err)
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/backblaze_b2/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["b2sdk"],
"quality_scale": "bronze",
"requirements": ["b2sdk==2.10.1"]
"requirements": ["b2sdk==2.10.4"]
}
4 changes: 4 additions & 0 deletions homeassistant/components/backblaze_b2/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"bad_request": "The Backblaze B2 API rejected the request: {error_message}",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_bucket_name": "[%key:component::backblaze_b2::exceptions::invalid_bucket_name::message%]",
"invalid_capability": "[%key:component::backblaze_b2::exceptions::invalid_capability::message%]",
Expand Down Expand Up @@ -60,6 +61,9 @@
}
},
"exceptions": {
"bad_request": {
"message": "The Backblaze B2 API rejected the request: {error_message}"
},
"cannot_connect": {
"message": "Cannot connect to endpoint"
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/backup/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "calculated",
"quality_scale": "internal",
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
"requirements": ["cronsim==2.7", "securetar==2026.4.0"],
"single_config_entry": true
}
5 changes: 4 additions & 1 deletion homeassistant/components/backup/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
SecureTarFile,
SecureTarReadError,
SecureTarRootKeyContext,
get_archive_max_ciphertext_size,
)

from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -431,7 +432,9 @@ def __init__(

def size(self) -> int:
"""Return the maximum size of the decrypted or encrypted backup."""
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
return get_archive_max_ciphertext_size( # type: ignore[no-any-return]
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
)

def _num_tar_files(self) -> int:
"""Return the number of inner tar files."""
Expand Down
31 changes: 22 additions & 9 deletions homeassistant/components/bsblan/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
BSBLAN,
BSBLANAuthError,
BSBLANConnectionError,
BSBLANError,
HotWaterConfig,
HotWaterSchedule,
HotWaterState,
Expand Down Expand Up @@ -50,7 +51,7 @@ class BSBLanFastData:

state: State
sensor: Sensor
dhw: HotWaterState
dhw: HotWaterState | None = None


@dataclass
Expand Down Expand Up @@ -111,7 +112,6 @@ async def _async_update_data(self) -> BSBLanFastData:
# This reduces response time significantly (~0.2s per parameter)
state = await self.client.state(include=STATE_INCLUDE)
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)

except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
Expand All @@ -126,6 +126,19 @@ async def _async_update_data(self) -> BSBLanFastData:
translation_placeholders={"host": host},
) from err

# Fetch DHW state separately - device may not support hot water
dhw: HotWaterState | None = None
try:
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
except BSBLANError:
# Preserve last known DHW state if available (entity may depend on it)
if self.data:
dhw = self.data.dhw
LOGGER.debug(
"DHW (Domestic Hot Water) state not available on device at %s",
self.config_entry.data[CONF_HOST],
)

return BSBLanFastData(
state=state,
sensor=sensor,
Expand Down Expand Up @@ -159,13 +172,6 @@ async def _async_update_data(self) -> BSBLanSlowData:
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
dhw_schedule = await self.client.hot_water_schedule()

except AttributeError:
# Device does not support DHW functionality
LOGGER.debug(
"DHW (Domestic Hot Water) not available on device at %s",
self.config_entry.data[CONF_HOST],
)
return BSBLanSlowData()
except (BSBLANConnectionError, BSBLANAuthError) as err:
# If config update fails, keep existing data
LOGGER.debug(
Expand All @@ -177,6 +183,13 @@ async def _async_update_data(self) -> BSBLanSlowData:
return self.data
# First fetch failed, return empty data
return BSBLanSlowData()
except BSBLANError, AttributeError:
# Device does not support DHW functionality
LOGGER.debug(
"DHW (Domestic Hot Water) not available on device at %s",
self.config_entry.data[CONF_HOST],
)
return BSBLanSlowData()

return BSBLanSlowData(
dhw_config=dhw_config,
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/bsblan/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ async def async_get_config_entry_diagnostics(
"fast_coordinator_data": {
"state": data.fast_coordinator.data.state.model_dump(),
"sensor": data.fast_coordinator.data.sensor.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump()
if data.fast_coordinator.data.dhw
else None,
},
"static": data.static.model_dump() if data.static is not None else None,
}
Expand Down
10 changes: 7 additions & 3 deletions homeassistant/components/bsblan/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

from yarl import URL

from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
Expand All @@ -10,7 +13,7 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import BSBLanData
from .const import DOMAIN
from .const import DEFAULT_PORT, DOMAIN
from .coordinator import BSBLanCoordinator, BSBLanFastCoordinator, BSBLanSlowCoordinator


Expand All @@ -22,7 +25,8 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
def __init__(self, coordinator: _T, data: BSBLanData) -> None:
"""Initialize BSBLan entity with device info."""
super().__init__(coordinator)
host = coordinator.config_entry.data["host"]
host = coordinator.config_entry.data[CONF_HOST]
port = coordinator.config_entry.data.get(CONF_PORT, DEFAULT_PORT)
mac = data.device.MAC
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac)},
Expand All @@ -44,7 +48,7 @@ def __init__(self, coordinator: _T, data: BSBLanData) -> None:
else None
),
sw_version=data.device.version,
configuration_url=f"http://{host}",
configuration_url=str(URL.build(scheme="http", host=host, port=port)),
)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/bsblan/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.1.3"],
"requirements": ["python-bsblan==5.1.4"],
"zeroconf": [
{
"name": "bsb-lan*",
Expand Down
Loading
Loading