Skip to content
Merged

2026.6.1 #173122

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
14968f9
Fix Lyric sensor crash when next_period_time is None (#167831)
bk86a Jun 4, 2026
53c77ae
Fix SleepIQ 401 storm by isolating client session cookies (#172276)
Stormalong Jun 4, 2026
4ca5da2
Upgrade HomeLink package, set integration type (#172371)
rjones-gentex Jun 3, 2026
74d2350
Bump actron-neo-api to 0.5.12 (#172902)
kclif9 Jun 3, 2026
706fea4
Switchbot Cloud: Fixed an issue where condition filtering for enabled…
XiaoLing-git Jun 4, 2026
43f6e79
Bump aioautomower to 2.7.6 (#172937)
Thomas55555 Jun 3, 2026
f81b6ab
Add more Reolink diagnostic info (#172945)
starkillerOG Jun 3, 2026
0387034
Fix Mitsubishi Comfort devices skipped due to unresolved local addres…
nikolairahimi Jun 5, 2026
106b189
Bump idasen-ha to 2.7.0 (#172962)
abmantis Jun 4, 2026
2961fca
Fix value template in MQTT Fan and Siren subentry setup (#172980)
jbouwh Jun 4, 2026
1f954cd
Improve person tests (#172997)
emontnemery Jun 4, 2026
6e7643e
Bump imgw_pib to 2.2.2 (#172999)
bieniu Jun 4, 2026
475ebbc
Fix person in_zones propagation from scanner in home zone (#173007)
emontnemery Jun 4, 2026
6a5dae9
Bump holidays to 0.98 (#173029)
gjohansson-ST Jun 4, 2026
06bf2ff
Portainer extend timeout for disk space coordinator (#173032)
erwindouna Jun 5, 2026
857a3de
Convert LinkPlay configuration_url to string for device registry (#17…
frenck Jun 4, 2026
74a4471
Fix Duco mode end time sensor name (#173045)
ronaldvdmeer Jun 5, 2026
4afced1
Unify query token auth in http views (#173082)
edenhaus Jun 5, 2026
18fa0ac
Create certificate files before trying to migrate the MQTT config ent…
jbouwh Jun 5, 2026
6cf1e7f
Bump wheels to 2026.06.0 (#173089)
edenhaus Jun 5, 2026
da7fa80
Bump pySmartThings to 4.0.1 (#173092)
joostlek Jun 5, 2026
af53d2d
Bump yoto-api to 3.1.6 (#173104)
piitaya Jun 5, 2026
e0a87d9
Bump aiostreammagic to 2.13.2 (#173114)
noahhusby Jun 5, 2026
0b77cf9
Fix process advertisement for active scans (#173116)
elupus Jun 5, 2026
0683344
Bump version to 2026.6.1
frenck Jun 5, 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
4 changes: 2 additions & 2 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
Expand Down Expand Up @@ -195,7 +195,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
Expand Down
4 changes: 2 additions & 2 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion homeassistant/components/actron_air/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.6"]
"requirements": ["actron-neo-api==0.5.12"]
}
18 changes: 12 additions & 6 deletions homeassistant/components/bluetooth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import asyncio
from asyncio import Future
from collections.abc import Callable, Iterable
from contextlib import ExitStack
from typing import TYPE_CHECKING, cast

from bleak import BleakScanner
Expand Down Expand Up @@ -178,15 +179,20 @@ def _async_discovered_device(
if not done.done() and callback(service_info):
done.set_result(service_info)

unload = _get_manager(hass).async_register_callback(
_async_discovered_device, match_dict, mode, scan_duration=timeout
)
manager = _get_manager(hass)

with ExitStack() as stack:
unload = manager.async_register_callback(
_async_discovered_device, match_dict, mode
)
stack.callback(unload)

if mode == BluetoothScanningMode.ACTIVE:
task = hass.async_create_task(manager.async_request_active_scan(timeout))
stack.callback(task.cancel)

try:
async with asyncio.timeout(timeout):
return await done
finally:
unload()


@hass_callback
Expand Down
28 changes: 10 additions & 18 deletions homeassistant/components/brands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""The Brands integration."""

from collections import deque
from collections.abc import Container, Mapping
from http import HTTPStatus
import logging
from pathlib import Path
from random import SystemRandom
import time
from typing import Any, Final
from typing import Any, Final, override

from aiohttp import ClientError, hdrs, web
from aiohttp import ClientError, web
import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback, valid_domain
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
Expand Down Expand Up @@ -108,23 +109,18 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
class _BrandsBaseView(HomeAssistantView):
"""Base view for serving brand images."""

requires_auth = False
use_query_token_for_auth = True

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the view."""
self._hass = hass
self._cache_dir = Path(hass.config.cache_path(DOMAIN))

def _authenticate(self, request: web.Request) -> None:
"""Authenticate the request using Bearer token or query token."""
access_tokens: deque[str] = self._hass.data[DOMAIN]
authenticated = (
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
raise web.HTTPForbidden
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
return self._hass.data[DOMAIN]

async def _serve_from_custom_integration(
self,
Expand Down Expand Up @@ -240,8 +236,6 @@ async def get(
image: str,
) -> web.Response:
"""Handle GET request for an integration brand image."""
self._authenticate(request)

if not valid_domain(domain) or image not in ALLOWED_IMAGES:
return web.Response(status=HTTPStatus.NOT_FOUND)

Expand Down Expand Up @@ -274,8 +268,6 @@ async def get(
image: str,
) -> web.Response:
"""Handle GET request for a hardware brand image."""
self._authenticate(request)

if not CATEGORY_RE.match(category):
return web.Response(status=HTTPStatus.NOT_FOUND)
# Hardware images have dynamic names like "manufacturer_model.png"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cambridge_audio/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"quality_scale": "platinum",
"requirements": ["aiostreammagic==2.13.1"],
"requirements": ["aiostreammagic==2.13.2"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
32 changes: 14 additions & 18 deletions homeassistant/components/camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
import collections
from collections.abc import Awaitable, Callable, Coroutine
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
from contextlib import suppress
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
Expand All @@ -12,16 +12,16 @@
import os
from random import SystemRandom
import time
from typing import Any, Final, final
from typing import Any, Final, final, override

from aiohttp import hdrs, web
from aiohttp import web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit

from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
Expand Down Expand Up @@ -776,30 +776,26 @@ def _async_write_ha_state(self) -> None:
class CameraView(HomeAssistantView):
"""Base CameraView."""

requires_auth = False
use_query_token_for_auth = True

def __init__(self, component: EntityComponent[Camera]) -> None:
"""Initialize a basic camera view."""
self.component = component

@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
return ()

return camera.access_tokens

async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
if (camera := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound

authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in camera.access_tokens
)

if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or camera access token
raise web.HTTPForbidden

if not camera.is_on:
_LOGGER.debug("Camera is off")
raise web.HTTPServiceUnavailable
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/duco/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"name": "Target flow level"
},
"time_state_end": {
"name": "Mode end time"
"name": "State end time"
},
"ventilation_state": {
"name": "Ventilation state",
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/gentex_homelink/manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"codeowners": ["@Gentex-Corporation/Homelink", "@rjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.1"]
"requirements": ["homelink-integration-api==0.0.5"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/holiday/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.97", "babel==2.15.0"]
"requirements": ["holidays==0.98", "babel==2.15.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/husqvarna_automower/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.7.5"]
"requirements": ["aioautomower==2.7.6"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/idasen_desk/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["idasen-ha==2.6.5"]
"requirements": ["idasen-ha==2.7.0"]
}
42 changes: 19 additions & 23 deletions homeassistant/components/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

import asyncio
import collections
from collections.abc import Container, Mapping
from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import os
from random import SystemRandom
from typing import Final, final
from typing import Final, final, override

from aiohttp import hdrs, web
from aiohttp import web
import httpx
from propcache.api import cached_property
import voluptuous as vol

from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
Expand Down Expand Up @@ -314,32 +315,27 @@ class ImageView(HomeAssistantView):
"""View to serve an image."""

name = "api:image:image"
requires_auth = False
use_query_token_for_auth = True
url = "/api/image_proxy/{entity_id}"

def __init__(self, component: EntityComponent[ImageEntity]) -> None:
"""Initialize an image view."""
self.component = component

async def _authenticate_request(
self, request: web.Request, entity_id: str
) -> ImageEntity:
"""Authenticate request and return image entity."""
if (image_entity := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (image_entity := self.component.get_entity(match_info["entity_id"])) is None:
return ()

authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in image_entity.access_tokens
)
return image_entity.access_tokens

if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or image entity access token
raise web.HTTPForbidden
@callback
def _get_image_entity(self, entity_id: str) -> ImageEntity:
"""Get image entity from request."""
if (image_entity := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound

return image_entity

Expand All @@ -349,7 +345,7 @@ async def head(self, request: web.Request, entity_id: str) -> web.Response:
This is sent by some DLNA renderers, like Samsung ones, prior to sending
the GET request.
"""
image_entity = await self._authenticate_request(request, entity_id)
image_entity = self._get_image_entity(entity_id)

# Don't use `handle` as we don't care about the stream case, we only want
# to verify that the image exists.
Expand All @@ -365,7 +361,7 @@ async def head(self, request: web.Request, entity_id: str) -> web.Response:

async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
image_entity = await self._authenticate_request(request, entity_id)
image_entity = self._get_image_entity(entity_id)
return await self.handle(request, image_entity)

async def handle(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/imgw_pib/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.2.0"]
"requirements": ["imgw_pib==2.2.2"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/linkplay/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(self, bridge: LinkPlayBridge) -> None:
)

self._attr_device_info = dr.DeviceInfo(
configuration_url=bridge.endpoint,
configuration_url=str(bridge.endpoint),
connections=connections,
hw_version=bridge.device.properties["hardware"],
identifiers={(DOMAIN, bridge.device.uuid)},
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/lyric/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,13 @@ def get_setpoint_status(status: str, time: str) -> str | None:
return LYRIC_SETPOINT_STATUS_NAMES.get(status)


def get_datetime_from_future_time(time_str: str) -> datetime:
def get_datetime_from_future_time(time_str: str | None) -> datetime | None:
"""Get datetime from future time provided."""
if time_str is None:
return None
time = dt_util.parse_time(time_str)
if time is None:
raise ValueError(f"Unable to parse time {time_str}")
return None
now = dt_util.utcnow()
if time <= now.time():
now = now + timedelta(days=1)
Expand Down
Loading
Loading