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() entry.unique_id for entry in self._async_current_entries()
} }
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError): with suppress(TimeoutError):
async with timeout(5): async with timeout(5):
hubs: list[aiopulse.Hub] = [ hubs = [
hub hub
async for hub in aiopulse.Hub.discover() async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured if hub.id not in already_configured

View File

@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(), ): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): 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 @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """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": "50D45C*" },
{ "macaddress": "50DCE7*" }, { "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" }, { "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "74D637*" }, { "macaddress": "74D637*" },
{ "macaddress": "7C6166*" }, { "macaddress": "7C6166*" },
{ "macaddress": "901195*" }, { "macaddress": "901195*" },
@ -22,7 +23,8 @@
{ "macaddress": "A8E621*" }, { "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" }, { "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" }, { "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" } { "macaddress": "EC2BEB*" },
{ "macaddress": "F02F9E*" }
], ],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices", "documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub", "integration_type": "hub",

View File

@ -5,7 +5,7 @@
"data_description_country": "The country of your Amazon account.", "data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address 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_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": { "config": {
"flow_title": "{username}", "flow_title": "{username}",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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.""" """The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """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] source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities( add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@ -1 +1,4 @@
"""The dlib_face_identify component.""" """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, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE 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 import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{ {
@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """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] confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES] faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE] source: list[dict[str, str]] = config[CONF_SOURCE]

View File

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

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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_IMPERIAL,
UNITS_METRIC, UNITS_METRIC,
) )
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema( RECONFIGURE_SCHEMA = vol.Schema(
{ {
@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN], user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION], user_input[CONF_DESTINATION],
) )
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException: except InvalidApiKeyException:
return {"base": "invalid_auth"} return {"base": "invalid_auth"}
except TimeoutError: except TimeoutError:

View File

@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden, Forbidden,
GatewayTimeout, GatewayTimeout,
GoogleAPIError, GoogleAPIError,
PermissionDenied,
Unauthorized, Unauthorized,
) )
from google.maps.routing_v2 import ( from google.maps.routing_v2 import (
@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2 from google.type import latlng_pb2
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv 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 homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try: try:
formatted_coordinates = coordinates.split(",") formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates)) vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid): except (AttributeError, vol.Invalid):
return Waypoint(address=location) return Waypoint(address=location)
return Waypoint( return Waypoint(
location=Location( location=Location(
@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes( await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)] 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: except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message) _LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error raise InvalidApiKeyException from unauthorized_error
@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception): class UnknownException(Exception):
"""Unknown API Error.""" """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 typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions 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 ( from google.maps.routing_v2 import (
ComputeRoutesRequest, ComputeRoutesRequest,
Route, Route,
@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM, TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_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__) _LOGGER = logging.getLogger(__name__)
@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes( response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)] request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
) )
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0: if response is not None and len(response.routes) > 0:
self._route = 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: except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex) _LOGGER.error("Error getting travel time: %s", ex)
self._route = None self._route = None

View File

@ -21,6 +21,7 @@
} }
}, },
"error": { "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%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers" "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.""" """The gstreamer component."""
DOMAIN = "gstreamer"

View File

@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP 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 import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline" CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Gstreamer platform.""" """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) name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE) pipeline = config.get(CONF_PIPELINE)

View File

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

View File

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

View File

@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP, 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 import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard" DOMAIN = "keyboard"
@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events.""" """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 = PyKeyboard()
keyboard.special_key_assignment() keyboard.special_key_assignment()

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "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", key="prebrew_on",
translation_key="prebrew_time_on", translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,
@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off", key="prebrew_off",
translation_key="prebrew_time_off", translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,

View File

@ -7,8 +7,9 @@ import time
import lirc import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP 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 import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability.""" """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) # blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully # also by not blocking, we allow hass to shut down the thread gracefully
# on exit. # on exit.

View File

@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty # don't discover this entry if the supported state list is empty
secondary_value_is_not=[], 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( MatterDiscoverySchema(
platform=Platform.SENSOR, platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription( entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths", key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position", translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100), measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,

View File

@ -390,12 +390,6 @@
"evse_user_max_charge_current": { "evse_user_max_charge_current": {
"name": "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": { "window_covering_target_position": {
"name": "Target opening position" "name": "Target opening position"
} }

View File

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

View File

@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS, DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
STATE_CLASS_UNITS,
STATE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA,
RestoreSensor, RestoreSensor,
SensorDeviceClass, SensorDeviceClass,
@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'" 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 ( if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None: ) is None:

View File

@ -644,6 +644,7 @@
"invalid_template": "Invalid template", "invalid_template": "Invalid template",
"invalid_supported_color_modes": "Invalid supported color modes selection", "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": "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", "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", "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", "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 self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by discovery.""" """Handle a flow initialized by discovery."""
await self._async_handle_discovery_without_unique_id()
return await self.async_step_user() return await self.async_step_user()

View File

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

View File

@ -27,10 +27,13 @@ from homeassistant.const import (
SERVICE_VOLUME_DOWN, SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_UP, 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -53,6 +56,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Pandora media player platform.""" """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(): if not _pianobar_exists():
return return
pandora = PandoraMediaPlayer("Pandora") pandora = PandoraMediaPlayer("Pandora")

View File

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

View File

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

View File

@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
SCAN_INTERVAL = timedelta(minutes=5) 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]]): class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):

View File

@ -40,7 +40,7 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"], "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": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -300,7 +300,9 @@ async def handle_call_service(
translation_placeholders=err.translation_placeholders, translation_placeholders=err.translation_placeholders,
) )
except HomeAssistantError as err: 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( connection.send_error(
msg["id"], msg["id"],
const.ERR_HOME_ASSISTANT_ERROR, const.ERR_HOME_ASSISTANT_ERROR,

View File

@ -170,8 +170,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
_title: str
def __init__(self) -> None: def __init__(self) -> None:
"""Set up flow instance.""" """Set up flow instance."""
self.s0_legacy_key: str | None = None self.s0_legacy_key: str | None = None
@ -446,7 +444,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
# at least for a short time. # at least for a short time.
return self.async_abort(reason="already_in_progress") return self.async_abort(reason="already_in_progress")
if current_config_entries := self._async_current_entries(include_ignore=False): if current_config_entries := self._async_current_entries(include_ignore=False):
config_entry = next( self._reconfigure_config_entry = next(
( (
entry entry
for entry in current_config_entries for entry in current_config_entries
@ -454,7 +452,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
), ),
None, None,
) )
if not config_entry: if not self._reconfigure_config_entry:
return self.async_abort(reason="addon_required") return self.async_abort(reason="addon_required")
vid = discovery_info.vid vid = discovery_info.vid
@ -503,31 +501,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
) )
title = human_name.split(" - ")[0].strip() title = human_name.split(" - ")[0].strip()
self.context["title_placeholders"] = {CONF_NAME: title} 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 self._usb_discovery = True
if current_config_entries := self._async_current_entries(include_ignore=False): if current_config_entries:
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")
return await self.async_step_intent_migrate() return await self.async_step_intent_migrate()
return await self.async_step_installation_type() return await self.async_step_installation_type()

View File

@ -98,9 +98,6 @@
"start_addon": { "start_addon": {
"title": "The Z-Wave add-on is starting." "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": { "zeroconf_confirm": {
"description": "Do you want to add the Z-Wave Server with home ID {home_id} found at {url} to Home Assistant?", "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" "title": "Discovered Z-Wave Server"
@ -134,7 +131,7 @@
}, },
"installation_type": { "installation_type": {
"title": "Set up Z-Wave", "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": { "menu_options": {
"intent_recommended": "Recommended installation", "intent_recommended": "Recommended installation",
"intent_custom": "Custom installation" "intent_custom": "Custom installation"

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "0.dev0" PATCH_VERSION: Final = "0b2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) 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 flow.cur_step = result
return result return result
# We pass a copy of the result because we're mutating our version try:
result = await self.async_finish_flow(flow, result.copy()) # 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 # _async_finish_flow may change result type, check it again
if result["type"] == FlowResultType.FORM: if result["type"] == FlowResultType.FORM:

View File

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

View File

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

View File

@ -64,10 +64,10 @@ def async_remove_stale_devices_links_keep_entity_device(
entry_id: str, entry_id: str,
source_entity_id_or_uuid: str, source_entity_id_or_uuid: str,
) -> None: ) -> 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 Also moves all entities linked to the entry_id to the device of
linked to the configuration entry will be maintained. source_entity_id_or_uuid.
""" """
async_remove_stale_devices_links_keep_current_device( async_remove_stale_devices_links_keep_current_device(
@ -83,13 +83,17 @@ def async_remove_stale_devices_links_keep_current_device(
entry_id: str, entry_id: str,
current_device_id: str | None, current_device_id: str | None,
) -> None: ) -> None:
"""Remove the link between stale devices and a configuration entry. """Remove entry_id from all devices except current_device_id."""
Only the device passed in the current_device_id parameter linked to
the configuration entry will be maintained.
"""
dev_reg = dr.async_get(hass) 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 # 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): for device in dev_reg.devices.get_devices_for_config_entry_id(entry_id):
if device.id == current_device_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: async def _on_hass_stop(_: Event) -> None:
"""Shutdown coordinator on HomeAssistant stop.""" """Shutdown coordinator on HomeAssistant stop."""
# Already cleared on EVENT_HOMEASSISTANT_STOP, via async_fire_internal
self._unsub_shutdown = None
await self.async_shutdown() await self.async_shutdown()
self._unsub_shutdown = self.hass.bus.async_listen_once( self._unsub_shutdown = self.hass.bus.async_listen_once(

View File

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

View File

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

4
requirements.txt generated
View File

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

10
requirements_all.txt generated
View File

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

View File

@ -783,6 +783,10 @@ evolutionhttp==0.0.18
# homeassistant.components.faa_delays # homeassistant.components.faa_delays
faadelays==2023.9.1 faadelays==2023.9.1
# homeassistant.components.dlib_face_detect
# homeassistant.components.dlib_face_identify
# face-recognition==1.2.3
# homeassistant.components.fastdotcom # homeassistant.components.fastdotcom
fastdotcom==0.0.3 fastdotcom==0.0.3
@ -941,6 +945,9 @@ growattServer==1.6.0
# homeassistant.components.google_sheets # homeassistant.components.google_sheets
gspread==5.5.0 gspread==5.5.0
# homeassistant.components.gstreamer
gstreamer-player==1.1.2
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.5 guppy3==3.1.5
@ -994,10 +1001,10 @@ hole==0.8.0
holidays==0.73 holidays==0.73
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250526.0 home-assistant-frontend==20250528.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.5.7 home-assistant-intents==2025.5.28
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==2.0.1.1 homematicip==2.0.1.1
@ -1386,6 +1393,11 @@ peco==0.1.2
# homeassistant.components.escea # homeassistant.components.escea
pescea==1.0.12 pescea==1.0.12
# homeassistant.components.aruba
# homeassistant.components.cisco_ios
# homeassistant.components.pandora
pexpect==4.9.0
# homeassistant.components.modem_callerid # homeassistant.components.modem_callerid
phone-modem==0.1.1 phone-modem==0.1.1
@ -1714,7 +1726,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8 pykulersky==0.5.8
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
pylamarzocco==2.0.6 pylamarzocco==2.0.7
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==5.1.0 pylast==5.1.0
@ -2000,6 +2012,9 @@ python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay # homeassistant.components.linkplay
python-linkplay==0.2.8 python-linkplay==0.2.8
# homeassistant.components.lirc
# python-lirc==1.2.3
# homeassistant.components.matter # homeassistant.components.matter
python-matter-server==7.0.0 python-matter-server==7.0.0
@ -2083,6 +2098,9 @@ pytrydan==0.8.0
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
# homeassistant.components.keyboard
# pyuserinput==0.1.11
# homeassistant.components.vera # homeassistant.components.vera
pyvera==0.3.15 pyvera==0.3.15
@ -2165,7 +2183,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.13.3 reolink-aio==0.13.4
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.66 rflink==0.0.66
@ -2422,7 +2440,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
uiprotect==7.10.0 uiprotect==7.10.1
# homeassistant.components.landisgyr_heat_meter # homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7 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 \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \ -r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ 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 "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>" 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 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") @pytest.mark.usefixtures("mock_hub_run")
async def test_show_form_one_hub(hass: HomeAssistant, mock_hub_discover) -> None: async def test_show_form_one_hub(hass: HomeAssistant, mock_hub_discover) -> None:
"""Test that a config is created when one hub discovered.""" """Test that a config is created when one hub discovered."""

View File

@ -1,6 +1,6 @@
"""Amazon Devices tests const.""" """Amazon Devices tests const."""
TEST_CODE = 123123 TEST_CODE = "023123"
TEST_COUNTRY = "IT" TEST_COUNTRY = "IT"
TEST_PASSWORD = "fake_password" TEST_PASSWORD = "fake_password"
TEST_SERIAL_NUMBER = "echo_test_serial_number" 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 homeassistant.helpers import entity_registry as er
from . import setup_integration from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform 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 := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE 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 assert result["result"].unique_id == TEST_USERNAME
mock_amazon_devices_client.login_mode_interactive.assert_called_once_with("023123")
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -6,19 +6,21 @@ from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_MESSAGE, ATTR_MESSAGE,
DOMAIN as NOTIFY_DOMAIN, DOMAIN as NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE, 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import setup_integration 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") @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 := hass.states.get(entity_id))
assert state.state == now.isoformat() 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_OFF,
SERVICE_TURN_ON, 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 mock_amazon_devices_client.set_do_not_disturb.call_count == 2
assert (state := hass.states.get(entity_id)) assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF 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', 'original_name': 'Last update',
'platform': 'aquacell', 'platform': 'aquacell',
'previous_unique_id': None, 'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'last_update', 'translation_key': 'last_update',
'unique_id': 'DSN-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( async def test_subentry_does_not_support_reconfigure(
hass: HomeAssistant, client: TestClient hass: HomeAssistant, client: TestClient
) -> None: ) -> 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 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 import pytest
from homeassistant.components.google_travel_time.const import ( from homeassistant.components.google_travel_time.const import (
@ -98,6 +103,12 @@ async def test_minimum_fields(hass: HomeAssistant) -> None:
(GoogleAPIError("test"), "cannot_connect"), (GoogleAPIError("test"), "cannot_connect"),
(GatewayTimeout("Timeout error."), "timeout_connect"), (GatewayTimeout("Timeout error."), "timeout_connect"),
(Unauthorized("Invalid API key."), "invalid_auth"), (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( 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 unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory 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 from google.maps.routing_v2 import Units
import pytest 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.components.google_travel_time.sensor import SCAN_INTERVAL
from homeassistant.const import CONF_MODE, STATE_UNKNOWN from homeassistant.const import CONF_MODE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.util.unit_system import ( from homeassistant.util.unit_system import (
METRIC_SYSTEM, METRIC_SYSTEM,
US_CUSTOMARY_SYSTEM, US_CUSTOMARY_SYSTEM,
@ -170,3 +171,26 @@ async def test_sensor_exception(
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN
assert "Error getting travel time" in caplog.text 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"), ("identifier", "exception_msg"),
[ [
("unique_id", "No file name"), ("unique_id", "No file name"),
("unique_id/album_id", "No file name"), ("unique_id/albums/album_id", "No file name"),
("unique_id/album_id/asset_id/filename", "No file extension"), ("unique_id/albums/album_id/asset_id/filename", "No file extension"),
], ],
) )
async def test_resolve_media_bad_identifier( async def test_resolve_media_bad_identifier(
@ -64,12 +64,12 @@ async def test_resolve_media_bad_identifier(
("identifier", "url", "mime_type"), ("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", "/immich/unique_id/asset_id/filename.jpg/fullsize",
"image/jpeg", "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", "/immich/unique_id/asset_id/filename.png/fullsize",
"image/png", "image/png",
), ),
@ -95,13 +95,82 @@ async def test_browse_media_unconfigured(hass: HomeAssistant) -> None:
source = await async_get_media_source(hass) source = await async_get_media_source(hass)
item = MediaSourceItem( 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"): with pytest.raises(BrowseError, match="Immich is not configured"):
await source.async_browse_media(item) 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, hass: HomeAssistant,
mock_immich: Mock, mock_immich: Mock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
@ -124,7 +193,7 @@ async def test_browse_media_album_error(
source = await async_get_media_source(hass) 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) result = await source.async_browse_media(item)
assert result assert result
@ -132,59 +201,7 @@ async def test_browse_media_album_error(
assert len(result.children) == 0 assert len(result.children) == 0
async def test_browse_media_get_root( async def test_browse_media_get_album_items_error(
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(
hass: HomeAssistant, hass: HomeAssistant,
mock_immich: Mock, mock_immich: Mock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
@ -202,7 +219,7 @@ async def test_browse_media_get_items_error(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -223,7 +240,7 @@ async def test_browse_media_get_items_error(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -233,7 +250,7 @@ async def test_browse_media_get_items_error(
assert len(result.children) == 0 assert len(result.children) == 0
async def test_browse_media_get_items( async def test_browse_media_get_album_items(
hass: HomeAssistant, hass: HomeAssistant,
mock_immich: Mock, mock_immich: Mock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
@ -249,7 +266,7 @@ async def test_browse_media_get_items(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -259,7 +276,7 @@ async def test_browse_media_get_items(
media_file = result.children[0] media_file = result.children[0]
assert isinstance(media_file, BrowseMedia) assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == ( assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/" "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/"
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg"
) )
@ -276,7 +293,7 @@ async def test_browse_media_get_items(
media_file = result.children[1] media_file = result.children[1]
assert isinstance(media_file, BrowseMedia) assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == ( assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/" "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/"
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4"
) )

View File

@ -1,6 +1,6 @@
"""Test the JustNimbus config flow.""" """Test the JustNimbus config flow."""
from unittest.mock import patch from unittest.mock import MagicMock, patch
from justnimbus.exceptions import InvalidClientID, JustNimbusError from justnimbus.exceptions import InvalidClientID, JustNimbusError
import pytest import pytest
@ -132,7 +132,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
with patch( with patch(
"homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data", "homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data",
return_value=True, return_value=MagicMock(),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], 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, 'min': 0,
'mode': <NumberMode.AUTO: 'auto'>, 'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1, 'step': 0.1,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'number.mr012345_prebrew_off_time', 'entity_id': 'number.mr012345_prebrew_off_time',
@ -173,7 +173,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'prebrew_time_off', 'translation_key': 'prebrew_time_off',
'unique_id': 'MR012345_prebrew_off', 'unique_id': 'MR012345_prebrew_off',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# name: test_prebrew_on[Linea Micra] # name: test_prebrew_on[Linea Micra]
@ -185,7 +185,7 @@
'min': 0, 'min': 0,
'mode': <NumberMode.AUTO: 'auto'>, 'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1, 'step': 0.1,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'number.mr012345_prebrew_on_time', 'entity_id': 'number.mr012345_prebrew_on_time',
@ -232,7 +232,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'prebrew_time_on', 'translation_key': 'prebrew_time_on',
'unique_id': 'MR012345_prebrew_on', 'unique_id': 'MR012345_prebrew_on',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# name: test_preinfusion[Linea Micra] # 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', '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] # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -2617,6 +2425,159 @@
'state': '0.0', '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] # name: test_sensors[humidity_sensor][sensor.mock_humidity_sensor_humidity-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -2907,6 +2868,159 @@
'state': 'stopped', '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] # name: test_sensors[oven][sensor.mock_oven_current_phase-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ '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( async def test_sensors(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,

View File

@ -3038,7 +3038,15 @@ async def test_migrate_of_incompatible_config_entry(
{ {
"state_class": "measurement", "state_class": "measurement",
}, },
(), (
(
{
"state_class": "measurement_angle",
"unit_of_measurement": "deg",
},
{"unit_of_measurement": "invalid_uom_for_state_class"},
),
),
{ {
"state_topic": "test-topic", "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 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( @pytest.mark.parametrize(
("hass_config", "error_logged"), ("hass_config", "error_logged"),
[ [

View File

@ -1002,6 +1002,24 @@ async def test_dhcp_discovery(
assert result.get("reason") == "missing_credentials" 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)]) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)])
async def test_dhcp_discovery_with_creds( async def test_dhcp_discovery_with_creds(
hass: HomeAssistant, 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"]) @pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
async def test_call_service_error( async def test_call_service_error(
hass: HomeAssistant, websocket_client: MockHAClientWebSocket hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
websocket_client: MockHAClientWebSocket,
) -> None: ) -> None:
"""Test call service command with error.""" """Test call service command with error."""
caplog.set_level(logging.ERROR)
@callback @callback
def ha_error_call(_): 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_placeholders"] == {"option": "bla"}
assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_key"] == "custom_error"
assert msg["error"]["translation_domain"] == "test" assert msg["error"]["translation_domain"] == "test"
assert "Traceback" not in caplog.text
await websocket_client.send_json_auto_id( 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_placeholders"] == {"option": "bla"}
assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_key"] == "custom_error"
assert msg["error"]["translation_domain"] == "test" assert msg["error"]["translation_domain"] == "test"
assert "Traceback" not in caplog.text
await websocket_client.send_json_auto_id( await websocket_client.send_json_auto_id(
{ {
@ -592,6 +597,7 @@ async def test_call_service_error(
assert msg["success"] is False assert msg["success"] is False
assert msg["error"]["code"] == "unknown_error" assert msg["error"]["code"] == "unknown_error"
assert msg["error"]["message"] == "value_error" assert msg["error"]["message"] == "value_error"
assert "Traceback" in caplog.text
async def test_subscribe_unsubscribe_events( 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}, context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO, data=USB_DISCOVERY_INFO,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "usb_confirm" assert result["step_id"] == "installation_type"
result2 = await hass.config_entries.flow.async_init( result2 = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -664,13 +664,8 @@ async def test_usb_discovery(
context={"source": config_entries.SOURCE_USB}, context={"source": config_entries.SOURCE_USB},
data=usb_discovery_info, 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 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["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type" assert result["step_id"] == "installation_type"
assert result["menu_options"] == ["intent_recommended", "intent_custom"] 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}, context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO, 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 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["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type" assert result["step_id"] == "installation_type"
@ -932,12 +923,8 @@ async def test_usb_discovery_migration(
context={"source": config_entries.SOURCE_USB}, context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO, 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 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["type"] is FlowResultType.FORM
assert result["step_id"] == "intent_migrate" 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}, context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO, 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 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["type"] is FlowResultType.FORM
assert result["step_id"] == "intent_migrate" 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, data=first_usb_info,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "usb_confirm" assert result["step_id"] == "installation_type"
result2 = await hass.config_entries.flow.async_init( result2 = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USB}, context={"source": config_entries.SOURCE_USB},
data=USB_DISCOVERY_INFO, data=USB_DISCOVERY_INFO,
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.MENU
assert result2["step_id"] == "usb_confirm" assert result2["step_id"] == "installation_type"
usb_flows_in_progress = hass.config_entries.flow.async_progress_by_handler( usb_flows_in_progress = hass.config_entries.flow.async_progress_by_handler(
DOMAIN, match_context={"source": config_entries.SOURCE_USB} 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" 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: async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None:
"""Test usb discovery flow is aborted when there is no supervisor.""" """Test usb discovery flow is aborted when there is no supervisor."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -4635,13 +4571,8 @@ async def test_recommended_usb_discovery(
context={"source": config_entries.SOURCE_USB}, context={"source": config_entries.SOURCE_USB},
data=usb_discovery_info, 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 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["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type" assert result["step_id"] == "installation_type"
assert result["menu_options"] == ["intent_recommended", "intent_custom"] 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, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test cleaning works for entity.""" """Test cleaning works for entity."""
config_entry = MockConfigEntry(domain="hue") helper_config_entry = MockConfigEntry(domain="helper_integration")
config_entry.add_to_hass(hass) 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( current_device = device_registry.async_get_or_create(
identifiers={("test", "current_device")}, identifiers={("test", "current_device")},
connections={("mac", "30:31:32:33:34:00")}, 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")}, identifiers={("test", "stale_device_1")},
connections={("mac", "30:31:32:33:34:01")}, 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( device_registry.async_get_or_create(
identifiers={("test", "stale_device_2")}, identifiers={("test", "stale_device_2")},
connections={("mac", "30:31:32:33:34:02")}, 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( source_entity = entity_registry.async_get_or_create(
"sensor", "sensor",
"test", "host_integration",
"source", "source",
config_entry=config_entry, config_entry=host_config_entry,
device_id=current_device.id, device_id=current_device.id,
) )
await hass.async_block_till_done() assert entity_registry.async_get(source_entity.entity_id) is not None
assert entity_registry.async_get("sensor.test_source") is not None
devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( # Helper entity connected to a stale device
config_entry.entry_id 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) # 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( async_remove_stale_devices_links_keep_entity_device(
hass, hass,
entry_id=config_entry.entry_id, entry_id=helper_config_entry.entry_id,
source_entity_id_or_uuid=source_entity.entity_id, source_entity_id_or_uuid=source_entity.entity_id,
) )
devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( await hass.async_block_till_done()
config_entry.entry_id
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 # After cleanup, only one device is expected to be linked to the config entry, and
assert len(devices_config_entry) == 1 # the entities should exist and be linked to the current device
assert len(devices_helper_entry) == 1
assert current_device in devices_config_entry 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( 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( @pytest.mark.parametrize(
("unique_id", "expected_result"), ("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( async def test_entry_subentry_duplicate(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -886,8 +886,8 @@ async def test_show_progress_fires_only_when_changed(
) # change (description placeholder) ) # change (description placeholder)
async def test_abort_flow_exception(manager: MockFlowManager) -> None: async def test_abort_flow_exception_step(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works.""" """Test that the AbortFlow exception works in a step."""
@manager.mock_reg_handler("test") @manager.mock_reg_handler("test")
class TestFlow(data_entry_flow.FlowHandler): 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"} 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: async def test_init_unknown_flow(manager: MockFlowManager) -> None:
"""Test that UnknownFlow is raised when async_create_flow returns None.""" """Test that UnknownFlow is raised when async_create_flow returns None."""