Compare commits

...

43 Commits

Author SHA1 Message Date
Bram Kragten 17a0b4f3d0 Bump version to 2025.6.0b2 2025-05-28 23:18:38 +02:00
Bram Kragten d0d228d9f4 Update frontend to 20250528.0 (#145828)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-05-28 23:18:33 +02:00
Michael 309acb961b Fix Immich media source browsing with multiple config entries (#145823)
fix media source browsing with multiple config entries
2025-05-28 23:18:32 +02:00
Michael Hansen 12f8ebb3ea Bump intents to 2025.5.28 (#145816) 2025-05-28 23:18:32 +02:00
David Bonnes 612861061c Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809)
* initial commit

* a better approach

* Add comment
2025-05-28 23:18:31 +02:00
Robert Resch 83af5ec36b Deprecate keyboard integration (#145805) 2025-05-28 23:18:30 +02:00
starkillerOG 74102d0319 Bump reolink-aio to 0.13.4 (#145799) 2025-05-28 23:18:29 +02:00
Robert Resch fbd05a0fcf Deprecate lirc integration (#145797) 2025-05-28 23:18:29 +02:00
Robert Resch a53c786fe0 Deprecate pandora integration (#145785) 2025-05-28 23:18:28 +02:00
Josef Zweck eb2728e5b9 Fix uom for prebrew numbers in lamarzocco (#145772) 2025-05-28 23:18:27 +02:00
J. Diego Rodríguez Royo 3f17223387 Add more information about possible hostnames at Home Connect (#145770) 2025-05-28 23:18:26 +02:00
Robert Resch 74104cf107 Deprecate GStreamer integration (#145768) 2025-05-28 23:18:25 +02:00
Robert Resch 13b4879723 Deprecate dlib image processing integrations (#145767) 2025-05-28 23:18:25 +02:00
Erik Montnemery f1ec0b2c59 Handle late abort when creating subentry (#145765)
* Handle late abort when creating subentry

* Move error handling to the base class

* Narrow down expected error in test
2025-05-28 23:18:24 +02:00
Josef Zweck 6d44daf599 Bump pylamarzocco to 2.0.7 (#145763) 2025-05-28 23:18:23 +02:00
Joost Lekkerkerker 644a6f5569 Add more Amazon Devices DHCP matches (#145754) 2025-05-28 23:18:22 +02:00
Abílio Costa fb83396522 Add Shelly zwave virtual integration (#145749) 2025-05-28 23:18:22 +02:00
Raphael Hehl e825bd0bdb Bump uiprotect to version 7.10.1 (#145737)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-28 23:18:21 +02:00
G Johansson 61823ec7e2 Fix dns resolver error in dnsip config flow validation (#145735)
Fix dns resolver error in dnsip
2025-05-28 23:18:20 +02:00
Michael cd133cbbe3 Add level of collections in Immich media source tree (#145734)
* add layer for collections in media source tree

* re-arange tests, add test for collection layer

* fix
2025-05-28 23:18:19 +02:00
Erik Montnemery 0e7a1bb76c Make async_remove_stale_devices_links_keep_entity_device move entities (#145719)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-28 23:18:18 +02:00
Josef Zweck f86bf69ebc Update otp description for amazon_devices (#145701)
* Update otp description from amazon_devices

* separate

* Update strings.json
2025-05-28 23:18:18 +02:00
Jan Bouwhuis adddf330fd Ensure mqtt sensor unit of measurement validation for state class `measurement_angle` (#145648) 2025-05-28 23:18:17 +02:00
Bram Kragten 10adb57b83 Bump version to 2025.6.0b1 2025-05-27 22:16:13 +02:00
Bram Kragten 3160fe9abc Update frontend to 20250527.0 (#145741) 2025-05-27 22:14:02 +02:00
Erwin Douna 6adb27d173 Tado update mobile devices interval (#145738)
Update the mobile devices interval to five minutes
2025-05-27 22:14:01 +02:00
Joost Lekkerkerker 6e6aae2ea3 Fix unbound local variable in Acmeda config flow (#145729) 2025-05-27 22:14:00 +02:00
Kevin Stillhammer 41a140d16c Debug log the update response in google_travel_time (#145725)
Debug log the update response
2025-05-27 22:14:00 +02:00
Kevin Stillhammer 8880ab6498 Catch PermissionDenied(Route API disabled) in google_travel_time (#145722)
Catch PermissionDenied(Route API disabled)
2025-05-27 22:13:59 +02:00
Martin Hjelmare 389becc4f6 Disable advanced window cover position Matter sensor by default (#145713)
* Disable advanced window cover position Matter sensor by default

* Enanble disabled sensors in snapshot test
2025-05-27 22:13:58 +02:00
Martin Hjelmare 923530972a Remove static pin code length Matter sensors (#145711)
* Remove static Matter sensors

* Clean up translation strings
2025-05-27 22:13:57 +02:00
Martin Hjelmare b84850df9f Fix error stack trace for HomeAssistantError in websocket service call (#145699)
* Add test

* Fix error stack trace for HomeAssistantError in websocket service call
2025-05-27 22:13:56 +02:00
Joost Lekkerkerker 9e7dc1d11d Use string type for amazon devices OTP code (#145698) 2025-05-27 22:13:56 +02:00
Petar Petrov 2830ed6147 Change description on recommended/custom Z-Wave install step (#145688)
Change description on recommended/custom Z-WaveJS step
2025-05-27 22:13:55 +02:00
Petar Petrov bfa919d078 Remove confirm screen after Z-Wave usb discovery (#145682)
* Remove confirm screen after Z-Wave usb discovery

* Simplify async_step_usb
2025-05-27 22:13:54 +02:00
Jan Bouwhuis f09c28e61f Fix justnimbus CI test (#145681) 2025-05-27 22:13:54 +02:00
J. Nick Koston bfdba7713e Bump aiohttp to 3.12.2 (#145671) 2025-05-27 22:13:53 +02:00
Kevin Stillhammer d6cadc1e3f Support addresses with comma in google_travel_time (#145663)
Support addresses with comma
2025-05-27 22:13:52 +02:00
Joost Lekkerkerker 20a6a3f195 Handle Google Nest DHCP flows (#145658)
* Handle Google Nest DHCP flows

* Handle Google Nest DHCP flows
2025-05-27 22:13:51 +02:00
Joost Lekkerkerker f60de45b52 Fix Amazon devices offline handling (#145656) 2025-05-27 22:13:50 +02:00
Joost Lekkerkerker 77031d1ae4 Fix Aquacell snapshot (#145651) 2025-05-27 22:13:49 +02:00
Jan Bouwhuis 9483a88ee1 Fix translation for sensor measurement angle state class (#145649) 2025-05-27 22:13:48 +02:00
Franck Nijhof 3438a4f063
Bump version to 2025.6.0b0 2025-05-26 20:31:18 +00:00
88 changed files with 1423 additions and 537 deletions

View File

@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries()
}
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError):
async with timeout(5):
hubs: list[aiopulse.Hub] = [
hubs = [
hub
async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured

View File

@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
vol.Required(CONF_CODE): cv.string,
}
),
)

View File

@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._serial_num in self.coordinator.data
return (
super().available
and self._serial_num in self.coordinator.data
and self.device.online
)

View File

@ -13,6 +13,7 @@
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "74D637*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "901195*" },
@ -22,7 +23,8 @@
{ "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
{ "macaddress": "EC2BEB*" },
{ "macaddress": "F02F9E*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",

View File

@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address."
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
},
"config": {
"flow_title": "{username}",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
}

View File

@ -1 +1,3 @@
"""The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Detect",
},
)
source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@ -1 +1,4 @@
"""The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"

View File

@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{
@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Identify",
},
)
confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE]

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
from typing import Any
from typing import Any, Literal
import aiodns
from aiodns.error import DNSError
@ -62,16 +62,16 @@ async def async_validate_hostname(
"""Validate hostname."""
async def async_check(
hostname: str, resolver: str, qtype: str, port: int = 53
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
) -> bool:
"""Return if able to resolve hostname."""
result = False
result: bool = False
with contextlib.suppress(DNSError):
result = bool(
await aiodns.DNSResolver( # type: ignore[call-overload]
_resolver = aiodns.DNSResolver(
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
)
result = bool(await _resolver.query(hostname, qtype))
return result
result: dict[str, bool] = {}

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250526.0"]
"requirements": ["home-assistant-frontend==20250528.0"]
}

View File

@ -50,7 +50,12 @@ from .const import (
UNITS_IMPERIAL,
UNITS_METRIC,
)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema(
{
@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
)
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException:
return {"base": "invalid_auth"}
except TimeoutError:

View File

@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try:
formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid):
except (AttributeError, vol.Invalid):
return Waypoint(address=location)
return Waypoint(
location=Location(
@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error
@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception):
"""Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")

View File

@ -7,7 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import GoogleAPIError
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import (
ComputeRoutesRequest,
Route,
@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
from .helpers import convert_to_waypoint
from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
_LOGGER = logging.getLogger(__name__)
@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
)
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0:
self._route = response.routes[0]
delete_routes_api_disabled_issue(self.hass, self._config_entry)
except PermissionDenied:
_LOGGER.error("Routes API is disabled for this API key")
create_routes_api_disabled_issue(self.hass, self._config_entry)
self._route = None
except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex)
self._route = None

View File

@ -21,6 +21,7 @@
}
},
"error": {
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers"
}
}
},
"issues": {
"routes_api_disabled": {
"title": "The Routes API must be enabled",
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
}
}
}

View File

@ -1 +1,3 @@
"""The gstreamer component."""
DOMAIN = "gstreamer"

View File

@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Gstreamer platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "GStreamer",
},
)
name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE)

View File

@ -10,11 +10,11 @@
"macaddress": "C8D778*"
},
{
"hostname": "(bosch|siemens)-*",
"hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "siemens-*",
"hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*"
}
],

View File

@ -30,11 +30,8 @@ LOGGER = getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Immich media source."""
entries = hass.config_entries.async_entries(
DOMAIN, include_disabled=False, include_ignore=False
)
hass.http.register_view(ImmichMediaView(hass))
return ImmichMediaSource(hass, entries)
return ImmichMediaSource(hass)
class ImmichMediaSourceIdentifier:
@ -43,11 +40,12 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None:
"""Split identifier into parts."""
parts = identifier.split("/")
# coonfig_entry.unique_id/album_id/asset_it/filename
# config_entry.unique_id/collection/collection_id/asset_id/file_name
self.unique_id = parts[0]
self.album_id = parts[1] if len(parts) > 1 else None
self.asset_id = parts[2] if len(parts) > 2 else None
self.file_name = parts[3] if len(parts) > 2 else None
self.collection = parts[1] if len(parts) > 1 else None
self.collection_id = parts[2] if len(parts) > 2 else None
self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource):
@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
name = "Immich"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize Immich media source."""
super().__init__(DOMAIN)
self.hass = hass
self.entries = entries
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Immich is not configured")
return BrowseMediaSource(
domain=DOMAIN,
@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_immich(item),
*await self._async_build_immich(item, entries),
],
)
async def _async_build_immich(
self, item: MediaSourceItem
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [
BrowseMediaSource(
domain=DOMAIN,
@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
can_play=False,
can_expand=True,
)
for entry in self.entries
for entry in entries
]
identifier = ImmichMediaSourceIdentifier(item.identifier)
entry: ImmichConfigEntry | None = (
@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
assert entry
immich_api = entry.runtime_data.api
if identifier.album_id is None:
# Get Albums
if identifier.collection is None:
LOGGER.debug("Render all collections for %s", entry.title)
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}/albums",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="albums",
can_play=False,
can_expand=True,
)
]
if identifier.collection_id is None:
LOGGER.debug("Render all albums for %s", entry.title)
try:
albums = await immich_api.albums.async_get_all_albums()
except ImmichError:
@ -118,7 +130,7 @@ class ImmichMediaSource(MediaSource):
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}",
identifier=f"{identifier.unique_id}/albums/{album.album_id}",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=album.name,
@ -129,10 +141,14 @@ class ImmichMediaSource(MediaSource):
for album in albums
]
# Request items of album
LOGGER.debug(
"Render all assets of album %s for %s",
identifier.collection_id,
entry.title,
)
try:
album_info = await immich_api.albums.async_get_album_info(
identifier.album_id
identifier.collection_id
)
except ImmichError:
return []
@ -141,8 +157,8 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{identifier.unique_id}/albums/"
f"{identifier.collection_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
),
@ -161,8 +177,8 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource(
domain=DOMAIN,
identifier=(
f"{identifier.unique_id}/"
f"{identifier.album_id}/"
f"{identifier.unique_id}/albums/"
f"{identifier.collection_id}/"
f"{asset.asset_id}/"
f"{asset.file_name}"
),

View File

@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard"
@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Keyboard",
},
)
keyboard = PyKeyboard()
keyboard.special_key_assignment()

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.6"]
"requirements": ["pylamarzocco==2.0.7"]
}

View File

@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_on",
translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,
@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off",
translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=0,
native_max_value=10,

View File

@ -7,8 +7,9 @@ import time
import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "LIRC",
},
)
# blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully
# on exit.

View File

@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MinPINCodeLength",
translation_key="min_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MaxPINCodeLength",
translation_key="max_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE,

View File

@ -390,12 +390,6 @@
"evse_user_max_charge_current": {
"name": "User max charge current"
},
"min_pin_code_length": {
"name": "Min PIN code length"
},
"max_pin_code_length": {
"name": "Max PIN code length"
},
"window_covering_target_position": {
"name": "Target opening position"
}

View File

@ -39,6 +39,7 @@ from homeassistant.components.light import (
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
STATE_CLASS_UNITS,
SensorDeviceClass,
SensorStateClass,
)
@ -640,6 +641,13 @@ def validate_sensor_platform_config(
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
):
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"
return errors
@ -676,11 +684,19 @@ class PlatformField:
@callback
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
"""Return a context based unit of measurement selector."""
if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
return SelectSelector(
SelectSelectorConfig(
options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
sort=True,
custom_value=True,
)
)
if (
user_data is None
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
or device_class not in DEVICE_CLASS_UNITS
):
device_class := user_data.get(CONF_DEVICE_CLASS)
) is None or device_class not in DEVICE_CLASS_UNITS:
return TEXT_SELECTOR
return SelectSelector(
SelectSelectorConfig(

View File

@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASS_UNITS,
STATE_CLASSES_SCHEMA,
RestoreSensor,
SensorDeviceClass,
@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)
if (
(state_class := config.get(CONF_STATE_CLASS)) is not None
and state_class in STATE_CLASS_UNITS
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT))
not in STATE_CLASS_UNITS[state_class]
):
raise vol.Invalid(
f"The unit of measurement '{unit_of_measurement}' is not valid "
f"together with state class '{state_class}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:

View File

@ -644,6 +644,7 @@
"invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection",
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_url": "Invalid URL",
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",

View File

@ -446,4 +446,5 @@ class NestFlowHandler(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
await self._async_handle_discovery_without_unique_id()
return await self.async_step_user()

View File

@ -1 +1,3 @@
"""The pandora component."""
DOMAIN = "pandora"

View File

@ -27,10 +27,13 @@ from homeassistant.const import (
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_UP,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -53,6 +56,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Pandora media player platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Pandora",
},
)
if not _pianobar_exists():
return
pandora = PandoraMediaPlayer("Pandora")

View File

@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.3"]
"requirements": ["reolink-aio==0.13.4"]
}

View File

@ -135,7 +135,7 @@
"name": "State class",
"state": {
"measurement": "Measurement",
"measurement_angle": "Measurement Angle",
"measurement_angle": "Measurement angle",
"total": "Total",
"total_increasing": "Total increasing"
}

View File

@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
SCAN_INTERVAL = timedelta(minutes=5)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5)
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):

View File

@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -300,7 +300,9 @@ async def handle_call_service(
translation_placeholders=err.translation_placeholders,
)
except HomeAssistantError as err:
connection.logger.exception("Unexpected exception")
connection.logger.error(
"Error during service call to %s.%s: %s", msg["domain"], msg["service"], err
)
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,

View File

@ -170,8 +170,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
_title: str
def __init__(self) -> None:
"""Set up flow instance."""
self.s0_legacy_key: str | None = None
@ -446,7 +444,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
# at least for a short time.
return self.async_abort(reason="already_in_progress")
if current_config_entries := self._async_current_entries(include_ignore=False):
config_entry = next(
self._reconfigure_config_entry = next(
(
entry
for entry in current_config_entries
@ -454,7 +452,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
),
None,
)
if not config_entry:
if not self._reconfigure_config_entry:
return self.async_abort(reason="addon_required")
vid = discovery_info.vid
@ -503,31 +501,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
)
title = human_name.split(" - ")[0].strip()
self.context["title_placeholders"] = {CONF_NAME: title}
self._title = title
return await self.async_step_usb_confirm()
async def async_step_usb_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle USB Discovery confirmation."""
if user_input is None:
return self.async_show_form(
step_id="usb_confirm",
description_placeholders={CONF_NAME: self._title},
)
self._usb_discovery = True
if current_config_entries := self._async_current_entries(include_ignore=False):
self._reconfigure_config_entry = next(
(
entry
for entry in current_config_entries
if entry.data.get(CONF_USE_ADDON)
),
None,
)
if not self._reconfigure_config_entry:
return self.async_abort(reason="addon_required")
if current_config_entries:
return await self.async_step_intent_migrate()
return await self.async_step_installation_type()

View File

@ -98,9 +98,6 @@
"start_addon": {
"title": "The Z-Wave add-on is starting."
},
"usb_confirm": {
"description": "Do you want to set up {name} with the Z-Wave add-on?"
},
"zeroconf_confirm": {
"description": "Do you want to add the Z-Wave Server with home ID {home_id} found at {url} to Home Assistant?",
"title": "Discovered Z-Wave Server"
@ -134,7 +131,7 @@
},
"installation_type": {
"title": "Set up Z-Wave",
"description": "Choose the installation type for your Z-Wave integration.",
"description": "In a few steps, were going to set up your Home Assistant Connect ZWA-2. Home Assistant can automatically install and configure the recommended Z-Wave setup, or you can customize it.",
"menu_options": {
"intent_recommended": "Recommended installation",
"intent_custom": "Custom installation"

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@ -543,8 +543,17 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
flow.cur_step = result
return result
try:
# We pass a copy of the result because we're mutating our version
result = await self.async_finish_flow(flow, result.copy())
except AbortFlow as err:
result = self._flow_result(
type=FlowResultType.ABORT,
flow_id=flow.flow_id,
handler=flow.handler,
reason=err.reason,
description_placeholders=err.description_placeholders,
)
# _async_finish_flow may change result type, check it again
if result["type"] == FlowResultType.FORM:

View File

@ -62,6 +62,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"domain": "amazon_devices",
"macaddress": "68F63B*",
},
{
"domain": "amazon_devices",
"macaddress": "6C0C9A*",
},
{
"domain": "amazon_devices",
"macaddress": "74D637*",
@ -102,6 +106,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"domain": "amazon_devices",
"macaddress": "EC2BEB*",
},
{
"domain": "amazon_devices",
"macaddress": "F02F9E*",
},
{
"domain": "august",
"hostname": "connect",
@ -359,12 +367,12 @@ DHCP: Final[list[dict[str, str | bool]]] = [
},
{
"domain": "home_connect",
"hostname": "(bosch|siemens)-*",
"hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*",
},
{
"domain": "home_connect",
"hostname": "siemens-*",
"hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*",
},
{

View File

@ -5867,10 +5867,18 @@
"iot_class": "local_push"
},
"shelly": {
"name": "Shelly",
"name": "shelly",
"integrations": {
"shelly": {
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
"iot_class": "local_push",
"name": "Shelly"
}
},
"iot_standards": [
"zwave"
]
},
"shodan": {
"name": "Shodan",

View File

@ -64,10 +64,10 @@ def async_remove_stale_devices_links_keep_entity_device(
entry_id: str,
source_entity_id_or_uuid: str,
) -> None:
"""Remove the link between stale devices and a configuration entry.
"""Remove entry_id from all devices except that of source_entity_id_or_uuid.
Only the device passed in the source_entity_id_or_uuid parameter
linked to the configuration entry will be maintained.
Also moves all entities linked to the entry_id to the device of
source_entity_id_or_uuid.
"""
async_remove_stale_devices_links_keep_current_device(
@ -83,13 +83,17 @@ def async_remove_stale_devices_links_keep_current_device(
entry_id: str,
current_device_id: str | None,
) -> None:
"""Remove the link between stale devices and a configuration entry.
Only the device passed in the current_device_id parameter linked to
the configuration entry will be maintained.
"""
"""Remove entry_id from all devices except current_device_id."""
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
# Make sure all entities are linked to the correct device
for entity in ent_reg.entities.get_entries_for_config_entry_id(entry_id):
if entity.device_id == current_device_id:
continue
ent_reg.async_update_entity(entity.entity_id, device_id=current_device_id)
# Removes all devices from the config entry that are not the same as the current device
for device in dev_reg.devices.get_devices_for_config_entry_id(entry_id):
if device.id == current_device_id:

View File

@ -138,6 +138,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
async def _on_hass_stop(_: Event) -> None:
"""Shutdown coordinator on HomeAssistant stop."""
# Already cleared on EVENT_HOMEASSISTANT_STOP, via async_fire_internal
self._unsub_shutdown = None
await self.async_shutdown()
self._unsub_shutdown = self.hass.bus.async_listen_once(

View File

@ -6,7 +6,7 @@ aiodns==3.4.0
aiohasupervisor==0.3.1
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.2.3
aiohttp==3.12.1
aiohttp==3.12.2
aiohttp_cors==0.7.0
aiousbwatcher==1.1.1
aiozoneinfo==0.2.3
@ -38,8 +38,8 @@ habluetooth==3.48.2
hass-nabucasa==0.101.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250526.0
home-assistant-intents==2025.5.7
home-assistant-frontend==20250528.0
home-assistant-intents==2025.5.28
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.6.0.dev0"
version = "2025.6.0b2"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@ -28,7 +28,7 @@ dependencies = [
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11
"aiohasupervisor==0.3.1",
"aiohttp==3.12.1",
"aiohttp==3.12.2",
"aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.2.3",
"aiohttp-asyncmdnsresolver==0.1.1",
@ -66,7 +66,7 @@ dependencies = [
# onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"home-assistant-intents==2025.5.7",
"home-assistant-intents==2025.5.28",
"ifaddr==0.2.0",
"Jinja2==3.1.6",
"lru-dict==1.3.0",

4
requirements.txt generated
View File

@ -5,7 +5,7 @@
# Home Assistant Core
aiodns==3.4.0
aiohasupervisor==0.3.1
aiohttp==3.12.1
aiohttp==3.12.2
aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.2.3
aiohttp-asyncmdnsresolver==0.1.1
@ -27,7 +27,7 @@ hass-nabucasa==0.101.0
hassil==2.2.3
httpx==0.28.1
home-assistant-bluetooth==1.13.1
home-assistant-intents==2025.5.7
home-assistant-intents==2025.5.28
ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0

10
requirements_all.txt generated
View File

@ -1164,10 +1164,10 @@ hole==0.8.0
holidays==0.73
# homeassistant.components.frontend
home-assistant-frontend==20250526.0
home-assistant-frontend==20250528.0
# homeassistant.components.conversation
home-assistant-intents==2025.5.7
home-assistant-intents==2025.5.28
# homeassistant.components.homematicip_cloud
homematicip==2.0.1.1
@ -2096,7 +2096,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lamarzocco
pylamarzocco==2.0.6
pylamarzocco==2.0.7
# homeassistant.components.lastfm
pylast==5.1.0
@ -2652,7 +2652,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.13.3
reolink-aio==0.13.4
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.10.0
uiprotect==7.10.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@ -783,6 +783,10 @@ evolutionhttp==0.0.18
# homeassistant.components.faa_delays
faadelays==2023.9.1
# homeassistant.components.dlib_face_detect
# homeassistant.components.dlib_face_identify
# face-recognition==1.2.3
# homeassistant.components.fastdotcom
fastdotcom==0.0.3
@ -941,6 +945,9 @@ growattServer==1.6.0
# homeassistant.components.google_sheets
gspread==5.5.0
# homeassistant.components.gstreamer
gstreamer-player==1.1.2
# homeassistant.components.profiler
guppy3==3.1.5
@ -994,10 +1001,10 @@ hole==0.8.0
holidays==0.73
# homeassistant.components.frontend
home-assistant-frontend==20250526.0
home-assistant-frontend==20250528.0
# homeassistant.components.conversation
home-assistant-intents==2025.5.7
home-assistant-intents==2025.5.28
# homeassistant.components.homematicip_cloud
homematicip==2.0.1.1
@ -1386,6 +1393,11 @@ peco==0.1.2
# homeassistant.components.escea
pescea==1.0.12
# homeassistant.components.aruba
# homeassistant.components.cisco_ios
# homeassistant.components.pandora
pexpect==4.9.0
# homeassistant.components.modem_callerid
phone-modem==0.1.1
@ -1714,7 +1726,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8
# homeassistant.components.lamarzocco
pylamarzocco==2.0.6
pylamarzocco==2.0.7
# homeassistant.components.lastfm
pylast==5.1.0
@ -2000,6 +2012,9 @@ python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.8
# homeassistant.components.lirc
# python-lirc==1.2.3
# homeassistant.components.matter
python-matter-server==7.0.0
@ -2083,6 +2098,9 @@ pytrydan==0.8.0
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
# homeassistant.components.keyboard
# pyuserinput==0.1.11
# homeassistant.components.vera
pyvera==0.3.15
@ -2165,7 +2183,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.13.3
reolink-aio==0.13.4
# homeassistant.components.rflink
rflink==0.0.66
@ -2422,7 +2440,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.10.0
uiprotect==7.10.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \
PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"

View File

@ -49,6 +49,21 @@ async def test_show_form_no_hubs(hass: HomeAssistant, mock_hub_discover) -> None
assert len(mock_hub_discover.mock_calls) == 1
async def test_timeout_fetching_hub(hass: HomeAssistant, mock_hub_discover) -> None:
"""Test that flow aborts if no hubs are discovered."""
mock_hub_discover.side_effect = TimeoutError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
# Check we performed the discovery
assert len(mock_hub_discover.mock_calls) == 1
@pytest.mark.usefixtures("mock_hub_run")
async def test_show_form_one_hub(hass: HomeAssistant, mock_hub_discover) -> None:
"""Test that a config is created when one hub discovered."""

View File

@ -1,6 +1,6 @@
"""Amazon Devices tests const."""
TEST_CODE = 123123
TEST_CODE = "023123"
TEST_COUNTRY = "IT"
TEST_PASSWORD = "fake_password"
TEST_SERIAL_NUMBER = "echo_test_serial_number"

View File

@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@ -69,3 +70,34 @@ async def test_coordinator_data_update_fails(
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
async def test_offline_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test offline device handling."""
entity_id = "binary_sensor.echo_test_connectivity"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE

View File

@ -56,6 +56,7 @@ async def test_full_flow(
},
}
assert result["result"].unique_id == TEST_USERNAME
mock_amazon_devices_client.login_mode_interactive.assert_called_once_with("023123")
@pytest.mark.parametrize(

View File

@ -6,19 +6,21 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
from homeassistant.components.notify import (
ATTR_MESSAGE,
DOMAIN as NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@ -68,3 +70,34 @@ async def test_notify_send_message(
assert (state := hass.states.get(entity_id))
assert state.state == now.isoformat()
async def test_offline_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test offline device handling."""
entity_id = "notify.echo_test_announce"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE

View File

@ -12,7 +12,13 @@ from homeassistant.components.switch import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -89,3 +95,34 @@ async def test_switch_dnd(
assert mock_amazon_devices_client.set_do_not_disturb.call_count == 2
assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF
async def test_offline_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test offline device handling."""
entity_id = "switch.echo_test_do_not_disturb"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE

View File

@ -77,6 +77,7 @@
'original_name': 'Last update',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'last_update',
'unique_id': 'DSN-last_update',

View File

@ -1526,6 +1526,88 @@ async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None:
}
async def test_subentry_flow_abort_duplicate(hass: HomeAssistant, client) -> None:
"""Test we can handle a subentry flow raising due to unique_id collision."""
class TestFlow(core_ce.ConfigFlow):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_user(self, user_input=None):
return await self.async_step_finish()
async def async_step_finish(self, user_input=None):
if user_input:
return self.async_create_entry(
title="Mock title", data=user_input, unique_id="test"
)
return self.async_show_form(
step_id="finish", data_schema=vol.Schema({"enabled": bool})
)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
subentries_data=[
core_ce.ConfigSubentryData(
data={},
subentry_id="mock_id",
subentry_type="test",
title="Title",
unique_id="test",
)
],
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
with mock_config_flow("test", TestFlow):
url = "/api/config/config_entries/subentries/flow"
resp = await client.post(url, json={"handler": [entry.entry_id, "test"]})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data.pop("flow_id")
assert data == {
"type": "form",
"handler": ["test1", "test"],
"step_id": "finish",
"data_schema": [{"name": "enabled", "type": "boolean"}],
"description_placeholders": None,
"errors": None,
"last_step": None,
"preview": None,
}
with mock_config_flow("test", TestFlow):
resp = await client.post(
f"/api/config/config_entries/subentries/flow/{flow_id}",
json={"enabled": True},
)
assert resp.status == HTTPStatus.OK
entries = hass.config_entries.async_entries("test")
assert len(entries) == 1
data = await resp.json()
data.pop("flow_id")
assert data == {
"handler": ["test1", "test"],
"reason": "already_configured",
"type": "abort",
"description_placeholders": None,
}
async def test_subentry_does_not_support_reconfigure(
hass: HomeAssistant, client: TestClient
) -> None:

View File

@ -0,0 +1 @@
"""The dlib_face_detect component."""

View File

@ -0,0 +1,37 @@
"""Dlib Face Identity Image Processing Tests."""
from unittest.mock import Mock, patch
from homeassistant.components.dlib_face_detect import DOMAIN as DLIB_DOMAIN
from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN
from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@patch.dict("sys.modules", face_recognition=Mock())
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
assert await async_setup_component(
hass,
IMAGE_PROCESSING_DOMAIN,
{
IMAGE_PROCESSING_DOMAIN: [
{
CONF_PLATFORM: DLIB_DOMAIN,
CONF_SOURCE: [
{CONF_ENTITY_ID: "camera.test_camera"},
],
}
],
},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}",
) in issue_registry.issues

View File

@ -0,0 +1 @@
"""The dlib_face_identify component."""

View File

@ -0,0 +1,41 @@
"""Dlib Face Identity Image Processing Tests."""
from unittest.mock import Mock, patch
from homeassistant.components.dlib_face_identify import (
CONF_FACES,
DOMAIN as DLIB_DOMAIN,
)
from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN
from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@patch.dict("sys.modules", face_recognition=Mock())
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
assert await async_setup_component(
hass,
IMAGE_PROCESSING_DOMAIN,
{
IMAGE_PROCESSING_DOMAIN: [
{
CONF_PLATFORM: DLIB_DOMAIN,
CONF_SOURCE: [
{CONF_ENTITY_ID: "camera.test_camera"},
],
CONF_FACES: {"person1": __file__},
}
],
},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}",
) in issue_registry.issues

View File

@ -2,7 +2,12 @@
from unittest.mock import AsyncMock, patch
from google.api_core.exceptions import GatewayTimeout, GoogleAPIError, Unauthorized
from google.api_core.exceptions import (
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
import pytest
from homeassistant.components.google_travel_time.const import (
@ -98,6 +103,12 @@ async def test_minimum_fields(hass: HomeAssistant) -> None:
(GoogleAPIError("test"), "cannot_connect"),
(GatewayTimeout("Timeout error."), "timeout_connect"),
(Unauthorized("Invalid API key."), "invalid_auth"),
(
PermissionDenied(
"Requests to this API routes.googleapis.com method google.maps.routing.v2.Routes.ComputeRoutes are blocked."
),
"permission_denied",
),
],
)
async def test_errors(

View File

@ -0,0 +1,46 @@
"""Tests for google_travel_time.helpers."""
from google.maps.routing_v2 import Location, Waypoint
from google.type import latlng_pb2
import pytest
from homeassistant.components.google_travel_time import helpers
from homeassistant.core import HomeAssistant
@pytest.mark.parametrize(
("location", "expected_result"),
[
(
"12.34,56.78",
Waypoint(
location=Location(
lat_lng=latlng_pb2.LatLng(
latitude=12.34,
longitude=56.78,
)
)
),
),
(
"12.34, 56.78",
Waypoint(
location=Location(
lat_lng=latlng_pb2.LatLng(
latitude=12.34,
longitude=56.78,
)
)
),
),
("Some Address", Waypoint(address="Some Address")),
("Some Street 1, 12345 City", Waypoint(address="Some Street 1, 12345 City")),
],
)
def test_convert_to_waypoint_coordinates(
hass: HomeAssistant, location: str, expected_result: Waypoint
) -> None:
"""Test convert_to_waypoint returns correct Waypoint for coordinates or address."""
waypoint = helpers.convert_to_waypoint(hass, location)
assert waypoint == expected_result

View File

@ -3,7 +3,7 @@
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from google.api_core.exceptions import GoogleAPIError
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import Units
import pytest
@ -20,6 +20,7 @@ from homeassistant.components.google_travel_time.const import (
from homeassistant.components.google_travel_time.sensor import SCAN_INTERVAL
from homeassistant.const import CONF_MODE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.util.unit_system import (
METRIC_SYSTEM,
US_CUSTOMARY_SYSTEM,
@ -170,3 +171,26 @@ async def test_sensor_exception(
await hass.async_block_till_done()
assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN
assert "Error getting travel time" in caplog.text
@pytest.mark.parametrize(
("data", "options"),
[(MOCK_CONFIG, DEFAULT_OPTIONS)],
)
async def test_sensor_routes_api_disabled(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
routes_mock: AsyncMock,
mock_config: MockConfigEntry,
freezer: FrozenDateTimeFactory,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test that exception gets caught and issue created."""
routes_mock.compute_routes.side_effect = PermissionDenied("Errormessage")
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN
assert "Routes API is disabled for this API key" in caplog.text
assert len(issue_registry.issues) == 1

View File

@ -0,0 +1 @@
"""Gstreamer tests."""

View File

@ -0,0 +1,34 @@
"""Tests for the Gstreamer platform."""
from unittest.mock import Mock, patch
from homeassistant.components.gstreamer import DOMAIN as GSTREAMER_DOMAIN
from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@patch.dict("sys.modules", gsp=Mock())
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
assert await async_setup_component(
hass,
PLATFORM_DOMAIN,
{
PLATFORM_DOMAIN: [
{
CONF_PLATFORM: GSTREAMER_DOMAIN,
}
],
},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{GSTREAMER_DOMAIN}",
) in issue_registry.issues

View File

@ -44,8 +44,8 @@ async def test_get_media_source(hass: HomeAssistant) -> None:
("identifier", "exception_msg"),
[
("unique_id", "No file name"),
("unique_id/album_id", "No file name"),
("unique_id/album_id/asset_id/filename", "No file extension"),
("unique_id/albums/album_id", "No file name"),
("unique_id/albums/album_id/asset_id/filename", "No file extension"),
],
)
async def test_resolve_media_bad_identifier(
@ -64,12 +64,12 @@ async def test_resolve_media_bad_identifier(
("identifier", "url", "mime_type"),
[
(
"unique_id/album_id/asset_id/filename.jpg",
"unique_id/albums/album_id/asset_id/filename.jpg",
"/immich/unique_id/asset_id/filename.jpg/fullsize",
"image/jpeg",
),
(
"unique_id/album_id/asset_id/filename.png",
"unique_id/albums/album_id/asset_id/filename.png",
"/immich/unique_id/asset_id/filename.png/fullsize",
"image/png",
),
@ -95,13 +95,82 @@ async def test_browse_media_unconfigured(hass: HomeAssistant) -> None:
source = await async_get_media_source(hass)
item = MediaSourceItem(
hass, DOMAIN, "unique_id/album_id/asset_id/filename.png", None
hass, DOMAIN, "unique_id/albums/album_id/asset_id/filename.png", None
)
with pytest.raises(BrowseError, match="Immich is not configured"):
await source.async_browse_media(item)
async def test_browse_media_album_error(
async def test_browse_media_get_root(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browse_media returning root media sources."""
assert await async_setup_component(hass, "media_source", {})
with patch("homeassistant.components.immich.PLATFORMS", []):
await setup_integration(hass, mock_config_entry)
source = await async_get_media_source(hass)
# get root
item = MediaSourceItem(hass, DOMAIN, "", None)
result = await source.async_browse_media(item)
assert result
assert len(result.children) == 1
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.title == "Someone"
assert media_file.media_content_id == (
"media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e"
)
# get collections
item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None)
result = await source.async_browse_media(item)
assert result
assert len(result.children) == 1
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.title == "albums"
assert media_file.media_content_id == (
"media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums"
)
async def test_browse_media_get_albums(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browse_media returning albums."""
assert await async_setup_component(hass, "media_source", {})
with patch("homeassistant.components.immich.PLATFORMS", []):
await setup_integration(hass, mock_config_entry)
source = await async_get_media_source(hass)
item = MediaSourceItem(
hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None
)
result = await source.async_browse_media(item)
assert result
assert len(result.children) == 1
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.title == "My Album"
assert media_file.media_content_id == (
"media-source://immich/"
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"
)
async def test_browse_media_get_albums_error(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
@ -124,7 +193,7 @@ async def test_browse_media_album_error(
source = await async_get_media_source(hass)
item = MediaSourceItem(hass, DOMAIN, mock_config_entry.unique_id, None)
item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None)
result = await source.async_browse_media(item)
assert result
@ -132,59 +201,7 @@ async def test_browse_media_album_error(
assert len(result.children) == 0
async def test_browse_media_get_root(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browse_media returning root media sources."""
assert await async_setup_component(hass, "media_source", {})
with patch("homeassistant.components.immich.PLATFORMS", []):
await setup_integration(hass, mock_config_entry)
source = await async_get_media_source(hass)
item = MediaSourceItem(hass, DOMAIN, "", None)
result = await source.async_browse_media(item)
assert result
assert len(result.children) == 1
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.title == "Someone"
assert media_file.media_content_id == (
"media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e"
)
async def test_browse_media_get_albums(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browse_media returning albums."""
assert await async_setup_component(hass, "media_source", {})
with patch("homeassistant.components.immich.PLATFORMS", []):
await setup_integration(hass, mock_config_entry)
source = await async_get_media_source(hass)
item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None)
result = await source.async_browse_media(item)
assert result
assert len(result.children) == 1
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.title == "My Album"
assert media_file.media_content_id == (
"media-source://immich/"
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"
)
async def test_browse_media_get_items_error(
async def test_browse_media_get_album_items_error(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
@ -202,7 +219,7 @@ async def test_browse_media_get_items_error(
item = MediaSourceItem(
hass,
DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None,
)
result = await source.async_browse_media(item)
@ -223,7 +240,7 @@ async def test_browse_media_get_items_error(
item = MediaSourceItem(
hass,
DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None,
)
result = await source.async_browse_media(item)
@ -233,7 +250,7 @@ async def test_browse_media_get_items_error(
assert len(result.children) == 0
async def test_browse_media_get_items(
async def test_browse_media_get_album_items(
hass: HomeAssistant,
mock_immich: Mock,
mock_config_entry: MockConfigEntry,
@ -249,7 +266,7 @@ async def test_browse_media_get_items(
item = MediaSourceItem(
hass,
DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None,
)
result = await source.async_browse_media(item)
@ -259,7 +276,7 @@ async def test_browse_media_get_items(
media_file = result.children[0]
assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/"
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg"
)
@ -276,7 +293,7 @@ async def test_browse_media_get_items(
media_file = result.children[1]
assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/"
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4"
)

View File

@ -1,6 +1,6 @@
"""Test the JustNimbus config flow."""
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from justnimbus.exceptions import InvalidClientID, JustNimbusError
import pytest
@ -132,7 +132,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
with patch(
"homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data",
return_value=True,
return_value=MagicMock(),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],

View File

@ -0,0 +1 @@
"""Keyboard tests."""

View File

@ -0,0 +1,29 @@
"""Keyboard tests."""
from unittest.mock import Mock, patch
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@patch.dict("sys.modules", pykeyboard=Mock())
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
from homeassistant.components.keyboard import ( # pylint:disable=import-outside-toplevel
DOMAIN as KEYBOARD_DOMAIN,
)
assert await async_setup_component(
hass,
KEYBOARD_DOMAIN,
{KEYBOARD_DOMAIN: {}},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{KEYBOARD_DOMAIN}",
) in issue_registry.issues

View File

@ -126,7 +126,7 @@
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mr012345_prebrew_off_time',
@ -173,7 +173,7 @@
'supported_features': 0,
'translation_key': 'prebrew_time_off',
'unique_id': 'MR012345_prebrew_off',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_prebrew_on[Linea Micra]
@ -185,7 +185,7 @@
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.mr012345_prebrew_on_time',
@ -232,7 +232,7 @@
'supported_features': 0,
'translation_key': 'prebrew_time_on',
'unique_id': 'MR012345_prebrew_on',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_preinfusion[Linea Micra]

View File

@ -0,0 +1 @@
"""LIRC tests."""

View File

@ -0,0 +1,31 @@
"""Tests for the LIRC."""
from unittest.mock import Mock, patch
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@patch.dict("sys.modules", lirc=Mock())
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
from homeassistant.components.lirc import ( # pylint: disable=import-outside-toplevel
DOMAIN as LIRC_DOMAIN,
)
assert await async_setup_component(
hass,
LIRC_DOMAIN,
{
LIRC_DOMAIN: {},
},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{LIRC_DOMAIN}",
) in issue_registry.issues

View File

@ -1307,198 +1307,6 @@
'state': '180.0',
})
# ---
# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_door_lock_max_pin_code_length',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Max PIN code length',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_pin_code_length',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Door Lock Max PIN code length',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_door_lock_max_pin_code_length',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '8',
})
# ---
# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_door_lock_min_pin_code_length',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Min PIN code length',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'min_pin_code_length',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Door Lock Min PIN code length',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_door_lock_min_pin_code_length',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---
# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_door_lock_max_pin_code_length',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Max PIN code length',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_pin_code_length',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Door Lock Max PIN code length',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_door_lock_max_pin_code_length',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '8',
})
# ---
# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_door_lock_min_pin_code_length',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Min PIN code length',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'min_pin_code_length',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Door Lock Min PIN code length',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_door_lock_min_pin_code_length',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---
# name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -2617,6 +2425,159 @@
'state': '0.0',
})
# ---
# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_generic_switch_current_switch_position',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current switch position',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Generic Switch Current switch position',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_generic_switch_current_switch_position',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_generic_switch_current_switch_position_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current switch position (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Generic Switch Current switch position (1)',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_generic_switch_current_switch_position_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_generic_switch_fancy_button',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Fancy Button',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-2-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Generic Switch Fancy Button',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_generic_switch_fancy_button',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[humidity_sensor][sensor.mock_humidity_sensor_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -2907,6 +2868,159 @@
'state': 'stopped',
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.inovelli_config',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Config',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli Config',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.inovelli_config',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.inovelli_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Down',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli Down',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.inovelli_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.inovelli_up',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Up',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'switch_current_position',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-SwitchCurrentPosition-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli Up',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.inovelli_up',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[oven][sensor.mock_oven_current_phase-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -17,7 +17,7 @@ from .common import (
)
@pytest.mark.usefixtures("matter_devices")
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "matter_devices")
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,

View File

@ -3038,7 +3038,15 @@ async def test_migrate_of_incompatible_config_entry(
{
"state_class": "measurement",
},
(),
(
(
{
"state_class": "measurement_angle",
"unit_of_measurement": "deg",
},
{"unit_of_measurement": "invalid_uom_for_state_class"},
),
),
{
"state_topic": "test-topic",
},

View File

@ -995,6 +995,32 @@ async def test_invalid_state_class(
assert "expected SensorStateClass or one of" in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"state_class": "measurement_angle",
"unit_of_measurement": "deg",
}
}
}
],
)
async def test_invalid_state_class_with_unit_of_measurement(
mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture
) -> None:
"""Test state_class option with invalid unit of measurement."""
assert await mqtt_mock_entry()
assert (
"The unit of measurement 'deg' is not valid together with state class 'measurement_angle'"
in caplog.text
)
@pytest.mark.parametrize(
("hass_config", "error_logged"),
[

View File

@ -1002,6 +1002,24 @@ async def test_dhcp_discovery(
assert result.get("reason") == "missing_credentials"
@pytest.mark.parametrize(
("nest_test_config", "sdm_managed_topic", "device_access_project_id"),
[(TEST_CONFIG_APP_CREDS, True, "project-id-2")],
)
async def test_dhcp_discovery_already_setup(
hass: HomeAssistant, oauth: OAuthFixture, setup_platform
) -> None:
"""Exercise discovery dhcp with existing config entry."""
await setup_platform()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=FAKE_DHCP_DATA,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_dhcp_discovery_with_creds(
hass: HomeAssistant,

View File

@ -0,0 +1 @@
"""Padora component tests."""

View File

@ -0,0 +1,31 @@
"""Pandora media player tests."""
from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN
from homeassistant.components.pandora import DOMAIN as PANDORA_DOMAIN
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
assert await async_setup_component(
hass,
PLATFORM_DOMAIN,
{
PLATFORM_DOMAIN: [
{
CONF_PLATFORM: PANDORA_DOMAIN,
}
],
},
)
await hass.async_block_till_done()
assert (
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{PANDORA_DOMAIN}",
) in issue_registry.issues

View File

@ -514,9 +514,12 @@ async def test_call_service_schema_validation_error(
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
async def test_call_service_error(
hass: HomeAssistant, websocket_client: MockHAClientWebSocket
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
websocket_client: MockHAClientWebSocket,
) -> None:
"""Test call service command with error."""
caplog.set_level(logging.ERROR)
@callback
def ha_error_call(_):
@ -561,6 +564,7 @@ async def test_call_service_error(
assert msg["error"]["translation_placeholders"] == {"option": "bla"}
assert msg["error"]["translation_key"] == "custom_error"
assert msg["error"]["translation_domain"] == "test"
assert "Traceback" not in caplog.text
await websocket_client.send_json_auto_id(
{
@ -578,6 +582,7 @@ async def test_call_service_error(
assert msg["error"]["translation_placeholders"] == {"option": "bla"}
assert msg["error"]["translation_key"] == "custom_error"
assert msg["error"]["translation_domain"] == "test"
assert "Traceback" not in caplog.text
await websocket_client.send_json_auto_id(
{
@ -592,6 +597,7 @@ async def test_call_service_error(
assert msg["success"] is False
assert msg["error"]["code"] == "unknown_error"
assert msg["error"]["message"] == "value_error"
assert "Traceback" in caplog.text
async def test_subscribe_unsubscribe_events(

View File

@ -585,8 +585,8 @@ async def test_abort_hassio_discovery_with_existing_flow(hass: HomeAssistant) ->
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
@ -664,13 +664,8 @@ async def test_usb_discovery(
context={"source": config_entries.SOURCE_USB},
data=usb_discovery_info,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert result["description_placeholders"] == {"name": discovery_name}
assert mock_usb_serial_by_id.call_count == 1
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
assert result["menu_options"] == ["intent_recommended", "intent_custom"]
@ -771,12 +766,8 @@ async def test_usb_discovery_addon_not_running(
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert mock_usb_serial_by_id.call_count == 2
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
@ -932,12 +923,8 @@ async def test_usb_discovery_migration(
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert mock_usb_serial_by_id.call_count == 2
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "intent_migrate"
@ -1063,12 +1050,8 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout(
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert mock_usb_serial_by_id.call_count == 2
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "intent_migrate"
@ -1366,16 +1349,16 @@ async def test_usb_discovery_with_existing_usb_flow(hass: HomeAssistant) -> None
data=first_usb_info,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "usb_confirm"
assert result2["type"] is FlowResultType.MENU
assert result2["step_id"] == "installation_type"
usb_flows_in_progress = hass.config_entries.flow.async_progress_by_handler(
DOMAIN, match_context={"source": config_entries.SOURCE_USB}
@ -1409,53 +1392,6 @@ async def test_abort_usb_discovery_addon_required(hass: HomeAssistant) -> None:
assert result["reason"] == "addon_required"
@pytest.mark.usefixtures(
"supervisor",
"addon_running",
)
async def test_abort_usb_discovery_confirm_addon_required(
hass: HomeAssistant,
addon_options: dict[str, Any],
mock_usb_serial_by_id: MagicMock,
) -> None:
"""Test usb discovery confirm aborted when existing entry not using add-on."""
addon_options["device"] = "/dev/another_device"
entry = MockConfigEntry(
domain=DOMAIN,
data={
"url": "ws://localhost:3000",
"usb_path": "/dev/another_device",
"use_addon": True,
},
title=TITLE,
unique_id="1234",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert mock_usb_serial_by_id.call_count == 2
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
"use_addon": False,
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "addon_required"
async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None:
"""Test usb discovery flow is aborted when there is no supervisor."""
result = await hass.config_entries.flow.async_init(
@ -4635,13 +4571,8 @@ async def test_recommended_usb_discovery(
context={"source": config_entries.SOURCE_USB},
data=usb_discovery_info,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usb_confirm"
assert result["description_placeholders"] == {"name": discovery_name}
assert mock_usb_serial_by_id.call_count == 1
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
assert result["menu_options"] == ["intent_recommended", "intent_custom"]

View File

@ -118,61 +118,75 @@ async def test_remove_stale_device_links_keep_entity_device(
entity_registry: er.EntityRegistry,
) -> None:
"""Test cleaning works for entity."""
config_entry = MockConfigEntry(domain="hue")
config_entry.add_to_hass(hass)
helper_config_entry = MockConfigEntry(domain="helper_integration")
helper_config_entry.add_to_hass(hass)
host_config_entry = MockConfigEntry(domain="host_integration")
host_config_entry.add_to_hass(hass)
current_device = device_registry.async_get_or_create(
identifiers={("test", "current_device")},
connections={("mac", "30:31:32:33:34:00")},
config_entry_id=config_entry.entry_id,
config_entry_id=helper_config_entry.entry_id,
)
assert current_device is not None
device_registry.async_get_or_create(
stale_device_1 = device_registry.async_get_or_create(
identifiers={("test", "stale_device_1")},
connections={("mac", "30:31:32:33:34:01")},
config_entry_id=config_entry.entry_id,
config_entry_id=helper_config_entry.entry_id,
)
device_registry.async_get_or_create(
identifiers={("test", "stale_device_2")},
connections={("mac", "30:31:32:33:34:02")},
config_entry_id=config_entry.entry_id,
config_entry_id=helper_config_entry.entry_id,
)
# Source entity registry
# Source entity
source_entity = entity_registry.async_get_or_create(
"sensor",
"test",
"host_integration",
"source",
config_entry=config_entry,
config_entry=host_config_entry,
device_id=current_device.id,
)
await hass.async_block_till_done()
assert entity_registry.async_get("sensor.test_source") is not None
assert entity_registry.async_get(source_entity.entity_id) is not None
devices_config_entry = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
# Helper entity connected to a stale device
helper_entity = entity_registry.async_get_or_create(
"sensor",
"helper_integration",
"helper",
config_entry=helper_config_entry,
device_id=stale_device_1.id,
)
assert entity_registry.async_get(helper_entity.entity_id) is not None
devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id(
helper_config_entry.entry_id
)
# 3 devices linked to the config entry are expected (1 current device + 2 stales)
assert len(devices_config_entry) == 3
assert len(devices_helper_entry) == 3
# Manual cleanup should unlink stales devices from the config entry
# Manual cleanup should unlink stale devices from the config entry
async_remove_stale_devices_links_keep_entity_device(
hass,
entry_id=config_entry.entry_id,
entry_id=helper_config_entry.entry_id,
source_entity_id_or_uuid=source_entity.entity_id,
)
devices_config_entry = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
await hass.async_block_till_done()
devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id(
helper_config_entry.entry_id
)
# After cleanup, only one device is expected to be linked to the config entry
assert len(devices_config_entry) == 1
assert current_device in devices_config_entry
# After cleanup, only one device is expected to be linked to the config entry, and
# the entities should exist and be linked to the current device
assert len(devices_helper_entry) == 1
assert current_device in devices_helper_entry
assert entity_registry.async_get(source_entity.entity_id) is not None
assert entity_registry.async_get(helper_entity.entity_id) is not None
async def test_remove_stale_devices_links_keep_current_device(

View File

@ -2226,7 +2226,7 @@ async def test_entry_subentry_no_context(
@pytest.mark.parametrize(
("unique_id", "expected_result"),
[(None, does_not_raise()), ("test", pytest.raises(HomeAssistantError))],
[(None, does_not_raise()), ("test", pytest.raises(data_entry_flow.AbortFlow))],
)
async def test_entry_subentry_duplicate(
hass: HomeAssistant,

View File

@ -886,8 +886,8 @@ async def test_show_progress_fires_only_when_changed(
) # change (description placeholder)
async def test_abort_flow_exception(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works."""
async def test_abort_flow_exception_step(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works in a step."""
@manager.mock_reg_handler("test")
class TestFlow(data_entry_flow.FlowHandler):
@ -900,6 +900,33 @@ async def test_abort_flow_exception(manager: MockFlowManager) -> None:
assert form["description_placeholders"] == {"placeholder": "yo"}
async def test_abort_flow_exception_finish_flow(hass: HomeAssistant) -> None:
"""Test that the AbortFlow exception works when finishing a flow."""
class TestFlow(data_entry_flow.FlowHandler):
VERSION = 1
async def async_step_init(self, input):
"""Return init form with one input field 'count'."""
return self.async_create_entry(title="init", data=input)
class FlowManager(data_entry_flow.FlowManager):
async def async_create_flow(self, handler_key, *, context, data):
"""Create a test flow."""
return TestFlow()
async def async_finish_flow(self, flow, result):
"""Raise AbortFlow."""
raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"})
manager = FlowManager(hass)
form = await manager.async_init("test")
assert form["type"] == data_entry_flow.FlowResultType.ABORT
assert form["reason"] == "mock-reason"
assert form["description_placeholders"] == {"placeholder": "yo"}
async def test_init_unknown_flow(manager: MockFlowManager) -> None:
"""Test that UnknownFlow is raised when async_create_flow returns None."""