Compare commits
43 Commits
dev
...
2025.6.0b2
Author | SHA1 | Date |
---|---|---|
|
17a0b4f3d0 | |
|
d0d228d9f4 | |
|
309acb961b | |
|
12f8ebb3ea | |
|
612861061c | |
|
83af5ec36b | |
|
74102d0319 | |
|
fbd05a0fcf | |
|
a53c786fe0 | |
|
eb2728e5b9 | |
|
3f17223387 | |
|
74104cf107 | |
|
13b4879723 | |
|
f1ec0b2c59 | |
|
6d44daf599 | |
|
644a6f5569 | |
|
fb83396522 | |
|
e825bd0bdb | |
|
61823ec7e2 | |
|
cd133cbbe3 | |
|
0e7a1bb76c | |
|
f86bf69ebc | |
|
adddf330fd | |
|
10adb57b83 | |
|
3160fe9abc | |
|
6adb27d173 | |
|
6e6aae2ea3 | |
|
41a140d16c | |
|
8880ab6498 | |
|
389becc4f6 | |
|
923530972a | |
|
b84850df9f | |
|
9e7dc1d11d | |
|
2830ed6147 | |
|
bfa919d078 | |
|
f09c28e61f | |
|
bfdba7713e | |
|
d6cadc1e3f | |
|
20a6a3f195 | |
|
f60de45b52 | |
|
77031d1ae4 | |
|
9483a88ee1 | |
|
3438a4f063 |
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"domain": "shelly",
|
||||||
|
"name": "shelly",
|
||||||
|
"integrations": ["shelly"],
|
||||||
|
"iot_standards": ["zwave"]
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The dlib_face_detect component."""
|
"""The dlib_face_detect component."""
|
||||||
|
|
||||||
|
DOMAIN = "dlib_face_detect"
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
"""The dlib_face_identify component."""
|
"""The dlib_face_identify component."""
|
||||||
|
|
||||||
|
CONF_FACES = "faces"
|
||||||
|
DOMAIN = "dlib_face_identify"
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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] = {}
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The gstreamer component."""
|
"""The gstreamer component."""
|
||||||
|
|
||||||
|
DOMAIN = "gstreamer"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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*"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -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}"
|
||||||
),
|
),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The pandora component."""
|
"""The pandora component."""
|
||||||
|
|
||||||
|
DOMAIN = "pandora"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]]):
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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, we’re 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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>"
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""The dlib_face_detect component."""
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
"""The dlib_face_identify component."""
|
|
@ -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
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Gstreamer tests."""
|
|
@ -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
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Keyboard tests."""
|
|
@ -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
|
|
@ -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]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""LIRC tests."""
|
|
@ -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
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"),
|
||||||
[
|
[
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Padora component tests."""
|
|
@ -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
|
|
@ -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(
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue