forked from home-assistant/core
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c705736739 | |||
| b15c9ad130 | |||
| 0128d85999 | |||
| e69ca0cf80 | |||
| 0719753be3 | |||
| ba3181d4e7 | |||
| e58750555e | |||
| 026687299d | |||
| 3eed552c56 | |||
| 15a4514c7d | |||
| b5445c0061 | |||
| 1d0584a90d | |||
| 158b795c70 | |||
| 4994229215 | |||
| c022c32d2f | |||
| d2ef3ca100 | |||
| 00faadcfea | |||
| a6ff52b300 | |||
| da0d65ca5b | |||
| 2266e97417 | |||
| d471de5645 | |||
| 38674f0dc2 | |||
| b192ca4bad | |||
| 73a59523f5 | |||
| 05324dedd0 | |||
| f1e5f73d7e | |||
| 7b23f21712 | |||
| 4dde314338 | |||
| cba12fb598 | |||
| 63e38b4d8d | |||
| 7eded95315 | |||
| e493fe1105 | |||
| 646c230940 | |||
| 5276a3688e | |||
| 0616bf16f4 | |||
| fbe1811e2b | |||
| 2333c10915 |
@@ -55,7 +55,6 @@ from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
deprecated_function,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
@@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
||||
from .webrtc import (
|
||||
DATA_ICE_SERVERS,
|
||||
CameraWebRTCProvider,
|
||||
WebRTCAnswer,
|
||||
WebRTCAnswer, # noqa: F401
|
||||
WebRTCCandidate, # noqa: F401
|
||||
WebRTCClientConfiguration,
|
||||
WebRTCError,
|
||||
WebRTCError, # noqa: F401
|
||||
WebRTCMessage, # noqa: F401
|
||||
WebRTCSendMessage,
|
||||
async_get_supported_provider,
|
||||
@@ -473,9 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self.async_update_token()
|
||||
self._create_stream_lock: asyncio.Lock | None = None
|
||||
self._webrtc_provider: CameraWebRTCProvider | None = None
|
||||
self._supports_native_sync_webrtc = (
|
||||
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
||||
)
|
||||
self._supports_native_async_webrtc = (
|
||||
type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
@@ -579,15 +575,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
return None
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
"""Handle the WebRTC offer and return an answer.
|
||||
|
||||
This is used by cameras with CameraEntityFeature.STREAM
|
||||
and StreamType.WEB_RTC.
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
|
||||
) -> None:
|
||||
@@ -600,42 +587,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
if self._supports_native_sync_webrtc:
|
||||
try:
|
||||
answer = await deprecated_function(
|
||||
"async_handle_async_webrtc_offer",
|
||||
breaks_in_ha_version="2025.6",
|
||||
)(self.async_handle_web_rtc_offer)(offer_sdp)
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Error handling WebRTC offer: %s", ex)
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
str(ex),
|
||||
)
|
||||
)
|
||||
except TimeoutError:
|
||||
# This catch was already here and should stay through the deprecation
|
||||
_LOGGER.error("Timeout handling WebRTC offer")
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
"Timeout handling WebRTC offer",
|
||||
)
|
||||
)
|
||||
else:
|
||||
if answer:
|
||||
send_message(WebRTCAnswer(answer))
|
||||
else:
|
||||
_LOGGER.error("Error handling WebRTC offer: No answer")
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"webrtc_offer_failed",
|
||||
"No answer on WebRTC offer",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if self._webrtc_provider:
|
||||
await self._webrtc_provider.async_handle_async_webrtc_offer(
|
||||
self, offer_sdp, session_id, send_message
|
||||
@@ -764,9 +715,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
new_provider = None
|
||||
|
||||
# Skip all providers if the camera has a native WebRTC implementation
|
||||
if not (
|
||||
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
|
||||
):
|
||||
if not self._supports_native_async_webrtc:
|
||||
# Camera doesn't have a native WebRTC implementation
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
@@ -798,17 +747,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
|
||||
config = self._async_get_webrtc_client_configuration()
|
||||
|
||||
if not self._supports_native_sync_webrtc:
|
||||
# Until 2024.11, the frontend was not resolving any ice servers
|
||||
# The async approach was added 2024.11 and new integrations need to use it
|
||||
ice_servers = [
|
||||
server
|
||||
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
|
||||
for server in servers()
|
||||
]
|
||||
config.configuration.ice_servers.extend(ice_servers)
|
||||
|
||||
config.get_candidates_upfront = self._supports_native_sync_webrtc
|
||||
ice_servers = [
|
||||
server
|
||||
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
|
||||
for server in servers()
|
||||
]
|
||||
config.configuration.ice_servers.extend(ice_servers)
|
||||
|
||||
return config
|
||||
|
||||
@@ -838,7 +782,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
|
||||
if self._supports_native_async_webrtc:
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
else:
|
||||
|
||||
@@ -111,13 +111,11 @@ class WebRTCClientConfiguration:
|
||||
|
||||
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
|
||||
data_channel: str | None = None
|
||||
get_candidates_upfront: bool = False
|
||||
|
||||
def to_frontend_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict that can be used by the frontend."""
|
||||
data: dict[str, Any] = {
|
||||
"configuration": self.configuration.to_dict(),
|
||||
"getCandidatesUpfront": self.get_candidates_upfront,
|
||||
}
|
||||
if self.data_channel is not None:
|
||||
data["dataChannel"] = self.data_channel
|
||||
|
||||
@@ -43,6 +43,7 @@ VALID_REPAIR_TRANSLATION_KEYS = {
|
||||
"no_subscription",
|
||||
"warn_bad_custom_domain_configuration",
|
||||
"reset_bad_custom_domain_configuration",
|
||||
"subscription_expired",
|
||||
}
|
||||
|
||||
|
||||
@@ -404,7 +405,12 @@ class CloudClient(Interface):
|
||||
) -> None:
|
||||
"""Create a repair issue."""
|
||||
if translation_key not in VALID_REPAIR_TRANSLATION_KEYS:
|
||||
raise ValueError(f"Invalid translation key {translation_key}")
|
||||
_LOGGER.error(
|
||||
"Invalid translation key %s for repair issue %s",
|
||||
translation_key,
|
||||
identifier,
|
||||
)
|
||||
return
|
||||
async_create_issue(
|
||||
hass=self._hass,
|
||||
domain=DOMAIN,
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
"reset_bad_custom_domain_configuration": {
|
||||
"title": "Custom domain ignored",
|
||||
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. This domain has now been ignored and will not be used for Home Assistant Cloud. If you want to use this domain, please fix the DNS configuration and restart Home Assistant. If you do not need this anymore, you can remove it from the account page."
|
||||
},
|
||||
"subscription_expired": {
|
||||
"title": "Subscription has expired",
|
||||
"description": "Your Home Assistant Cloud subscription has expired. Resubscribe at {account_url}."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiocomelit==0.12.0"]
|
||||
"requirements": ["aiocomelit==0.12.1"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.1.1",
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"ac_module_temperature_sensor_faulty_l2": "AC module temperature sensor faulty (L2)",
|
||||
"dc_component_measured_in_grid_too_high": "DC component measured in the grid too high",
|
||||
"fixed_voltage_mode_out_of_range": "Fixed voltage mode has been selected instead of MPP voltage mode and the fixed voltage has been set to too low or too high a value",
|
||||
"safety_cut_out_triggered": "Safety cut out via option card or RECERBO has triggered",
|
||||
"safety_cut_out_triggered": "Safety cut-out via option card or RECERBO has triggered",
|
||||
"no_communication_between_power_stage_and_control_system": "No communication possible between power stage set and control system",
|
||||
"hardware_id_problem": "Hardware ID problem",
|
||||
"unique_id_conflict": "Unique ID conflict",
|
||||
@@ -148,7 +148,7 @@
|
||||
"update_file_does_not_match_device": "Update file does not match the device, update file too old",
|
||||
"write_or_read_error_occurred": "Write or read error occurred",
|
||||
"file_could_not_be_opened": "File could not be opened",
|
||||
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)",
|
||||
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write-protected or full)",
|
||||
"initialisation_error_file_system_error_on_usb": "Initialization error in file system on USB flash drive",
|
||||
"error_during_logging_data_recording": "Error during recording of logging data",
|
||||
"error_during_update_process": "Error occurred during update process",
|
||||
@@ -166,7 +166,7 @@
|
||||
"invalid_device_type": "Invalid device type",
|
||||
"insulation_measurement_triggered": "Insulation measurement triggered",
|
||||
"inverter_settings_changed_restart_required": "Inverter settings have been changed, inverter restart required",
|
||||
"wired_shut_down_triggered": "Wired shut down triggered",
|
||||
"wired_shut_down_triggered": "Wired shutdown triggered",
|
||||
"grid_frequency_exceeded_limit_reconnecting": "The grid frequency has exceeded a limit value when reconnecting",
|
||||
"mains_voltage_dependent_power_reduction": "Mains voltage-dependent power reduction",
|
||||
"too_little_dc_power_for_feed_in_operation": "Too little DC power for feed-in operation",
|
||||
|
||||
@@ -14,49 +14,78 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DATA_STORAGE: HassKey[tuple[dict[str, Store], dict[str, dict]]] = HassKey(
|
||||
"frontend_storage"
|
||||
)
|
||||
DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage")
|
||||
STORAGE_VERSION_USER_DATA = 1
|
||||
|
||||
|
||||
@callback
|
||||
def _initialize_frontend_storage(hass: HomeAssistant) -> None:
|
||||
"""Set up frontend storage."""
|
||||
if DATA_STORAGE in hass.data:
|
||||
return
|
||||
hass.data[DATA_STORAGE] = ({}, {})
|
||||
|
||||
|
||||
async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
|
||||
"""Set up frontend storage."""
|
||||
_initialize_frontend_storage(hass)
|
||||
websocket_api.async_register_command(hass, websocket_set_user_data)
|
||||
websocket_api.async_register_command(hass, websocket_get_user_data)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_user_data)
|
||||
|
||||
|
||||
async def async_user_store(
|
||||
hass: HomeAssistant, user_id: str
|
||||
) -> tuple[Store, dict[str, Any]]:
|
||||
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
|
||||
"""Access a user store."""
|
||||
_initialize_frontend_storage(hass)
|
||||
stores, data = hass.data[DATA_STORAGE]
|
||||
stores = hass.data.setdefault(DATA_STORAGE, {})
|
||||
if (store := stores.get(user_id)) is None:
|
||||
store = stores[user_id] = Store(
|
||||
store = stores[user_id] = UserStore(hass, user_id)
|
||||
await store.async_load()
|
||||
|
||||
return store
|
||||
|
||||
|
||||
class UserStore:
|
||||
"""User store for frontend data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, user_id: str) -> None:
|
||||
"""Initialize the user store."""
|
||||
self._store = _UserStore(hass, user_id)
|
||||
self.data: dict[str, Any] = {}
|
||||
self.subscriptions: dict[str | None, list[Callable[[], None]]] = {}
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the data from the store."""
|
||||
self.data = await self._store.async_load() or {}
|
||||
|
||||
async def async_set_item(self, key: str, value: Any) -> None:
|
||||
"""Set an item item and save the store."""
|
||||
self.data[key] = value
|
||||
await self._store.async_save(self.data)
|
||||
for cb in self.subscriptions.get(None, []):
|
||||
cb()
|
||||
for cb in self.subscriptions.get(key, []):
|
||||
cb()
|
||||
|
||||
@callback
|
||||
def async_subscribe(
|
||||
self, key: str | None, on_update_callback: Callable[[], None]
|
||||
) -> Callable[[], None]:
|
||||
"""Save the data to the store."""
|
||||
self.subscriptions.setdefault(key, []).append(on_update_callback)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
"""Unsubscribe from the store."""
|
||||
self.subscriptions[key].remove(on_update_callback)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
|
||||
class _UserStore(Store[dict[str, Any]]):
|
||||
"""User store for frontend data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, user_id: str) -> None:
|
||||
"""Initialize the user store."""
|
||||
super().__init__(
|
||||
hass,
|
||||
STORAGE_VERSION_USER_DATA,
|
||||
f"frontend.user_data_{user_id}",
|
||||
)
|
||||
|
||||
if user_id not in data:
|
||||
data[user_id] = await store.async_load() or {}
|
||||
|
||||
return store, data[user_id]
|
||||
|
||||
|
||||
def with_store(
|
||||
def with_user_store(
|
||||
orig_func: Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], Store, dict[str, Any]],
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], UserStore],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
) -> Callable[
|
||||
@@ -65,17 +94,17 @@ def with_store(
|
||||
"""Decorate function to provide data."""
|
||||
|
||||
@wraps(orig_func)
|
||||
async def with_store_func(
|
||||
async def with_user_store_func(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Provide user specific data and store to function."""
|
||||
user_id = connection.user.id
|
||||
|
||||
store, user_data = await async_user_store(hass, user_id)
|
||||
store = await async_user_store(hass, user_id)
|
||||
|
||||
await orig_func(hass, connection, msg, store, user_data)
|
||||
await orig_func(hass, connection, msg, store)
|
||||
|
||||
return with_store_func
|
||||
return with_user_store_func
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
@@ -86,41 +115,57 @@ def with_store(
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@with_store
|
||||
@with_user_store
|
||||
async def websocket_set_user_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: Store,
|
||||
data: dict[str, Any],
|
||||
store: UserStore,
|
||||
) -> None:
|
||||
"""Handle set global data command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
data[msg["key"]] = msg["value"]
|
||||
await store.async_save(data)
|
||||
connection.send_message(websocket_api.result_message(msg["id"]))
|
||||
"""Handle set user data command."""
|
||||
await store.async_set_item(msg["key"], msg["value"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "frontend/get_user_data", vol.Optional("key"): str}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@with_store
|
||||
@with_user_store
|
||||
async def websocket_get_user_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: Store,
|
||||
data: dict[str, Any],
|
||||
store: UserStore,
|
||||
) -> None:
|
||||
"""Handle get global data command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
connection.send_message(
|
||||
websocket_api.result_message(
|
||||
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
|
||||
)
|
||||
"""Handle get user data command."""
|
||||
data = store.data
|
||||
connection.send_result(
|
||||
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "frontend/subscribe_user_data", vol.Optional("key"): str}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@with_user_store
|
||||
async def websocket_subscribe_user_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: UserStore,
|
||||
) -> None:
|
||||
"""Handle subscribe to user data command."""
|
||||
key: str | None = msg.get("key")
|
||||
|
||||
def on_data_update() -> None:
|
||||
"""Handle user data update."""
|
||||
data = store.data
|
||||
connection.send_event(
|
||||
msg["id"], {"value": data.get(key) if key is not None else data}
|
||||
)
|
||||
|
||||
connection.subscriptions[msg["id"]] = store.async_subscribe(key, on_data_update)
|
||||
on_data_update()
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"]
|
||||
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import jwt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -58,3 +59,22 @@ class OAuth2FlowHandler(
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await super().async_oauth_create_entry(data)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a DHCP discovery."""
|
||||
device_registry = dr.async_get(self.hass)
|
||||
if device_entry := device_registry.async_get_device(
|
||||
identifiers={
|
||||
(DOMAIN, discovery_info.hostname),
|
||||
(DOMAIN, discovery_info.hostname.split("-")[-1]),
|
||||
}
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device_entry.id,
|
||||
new_connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
|
||||
},
|
||||
)
|
||||
return await super().async_step_dhcp(discovery_info)
|
||||
|
||||
@@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
|
||||
mower_attributes = self.mower_attributes
|
||||
if mower_attributes.mower.state in PAUSED_STATES:
|
||||
return LawnMowerActivity.PAUSED
|
||||
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
return LawnMowerActivity.MOWING
|
||||
if (mower_attributes.mower.state == "RESTRICTED") or (
|
||||
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||
):
|
||||
return LawnMowerActivity.DOCKED
|
||||
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
return LawnMowerActivity.MOWING
|
||||
return LawnMowerActivity.ERROR
|
||||
|
||||
@property
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"requirements": [
|
||||
"xknx==3.6.0",
|
||||
"xknx==3.8.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.4.1.91934"
|
||||
],
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -44,21 +43,6 @@ CONFIG_SCHEMA = vol.Schema(CONFIG_DATA)
|
||||
USER_SCHEMA = vol.Schema(USER_DATA)
|
||||
|
||||
|
||||
def get_config_entry(
|
||||
hass: HomeAssistant, data: ConfigType
|
||||
) -> config_entries.ConfigEntry | None:
|
||||
"""Check config entries for already configured entries based on the ip address/port."""
|
||||
return next(
|
||||
(
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.data[CONF_IP_ADDRESS] == data[CONF_IP_ADDRESS]
|
||||
and entry.data[CONF_PORT] == data[CONF_PORT]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
async def validate_connection(data: ConfigType) -> str | None:
|
||||
"""Validate if a connection to LCN can be established."""
|
||||
error = None
|
||||
@@ -120,19 +104,20 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)
|
||||
|
||||
errors = None
|
||||
if get_config_entry(self.hass, user_input):
|
||||
errors = {CONF_BASE: "already_configured"}
|
||||
elif (error := await validate_connection(user_input)) is not None:
|
||||
errors = {CONF_BASE: error}
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
}
|
||||
)
|
||||
|
||||
if errors is not None:
|
||||
if (error := await validate_connection(user_input)) is not None:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
USER_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
errors={CONF_BASE: error},
|
||||
)
|
||||
|
||||
data: dict = {
|
||||
@@ -152,15 +137,21 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
user_input[CONF_HOST] = reconfigure_entry.data[CONF_HOST]
|
||||
|
||||
await self.hass.config_entries.async_unload(reconfigure_entry.entry_id)
|
||||
if (error := await validate_connection(user_input)) is not None:
|
||||
errors = {CONF_BASE: error}
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
}
|
||||
)
|
||||
|
||||
if errors is None:
|
||||
await self.hass.config_entries.async_unload(reconfigure_entry.entry_id)
|
||||
|
||||
if (error := await validate_connection(user_input)) is None:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry, data_updates=user_input
|
||||
)
|
||||
|
||||
errors = {CONF_BASE: error}
|
||||
await self.hass.config_entries.async_setup(reconfigure_entry.entry_id)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
"error": {
|
||||
"authentication_error": "Authentication failed. Wrong username or password.",
|
||||
"license_error": "Maximum number of connections was reached. An additional licence key is required.",
|
||||
"connection_refused": "Unable to connect to PCHK. Check IP and port.",
|
||||
"already_configured": "PCHK connection using the same ip address/port is already configured."
|
||||
"connection_refused": "Unable to connect to PCHK. Check IP and port."
|
||||
},
|
||||
"abort": {
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"already_configured": "PCHK connection using the same ip address/port is already configured."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.2.1"]
|
||||
"requirements": ["ical==9.2.2"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.2.1"]
|
||||
"requirements": ["ical==9.2.2"]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ from .const import MieleAppliance
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from .const import DOMAIN, PROCESS_ACTION, MieleActions, MieleAppliance
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ from .const import DEVICE_TYPE_TAGS, DISABLED_TEMP_ENTITIES, DOMAIN, MieleApplia
|
||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ from .const import DOMAIN, POWER_OFF, POWER_ON, VENTILATION_STEP, MieleAppliance
|
||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_RANGE = (1, 4)
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
"core_target_temperature": {
|
||||
"default": "mdi:thermometer-probe"
|
||||
},
|
||||
"target_temperature": {
|
||||
"default": "mdi:thermometer-check"
|
||||
},
|
||||
"drying_step": {
|
||||
"default": "mdi:water-outline"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,8 @@ from .const import AMBIENT_LIGHT, DOMAIN, LIGHT, LIGHT_OFF, LIGHT_ON, MieleAppli
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleDevice, MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -32,18 +32,23 @@ rules:
|
||||
Handled by a setting in manifest.json as there is no account information in API
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions:
|
||||
status: done
|
||||
comment: No custom actions are defined
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No configuration parameters
|
||||
docs-installation-parameters: todo
|
||||
docs-installation-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration uses account linking via Nabu casa so no installation parameters are needed.
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: Handled by coordinator
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: Handled by DataUpdateCoordinator
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ from .const import (
|
||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISABLED_TEMPERATURE = -32768
|
||||
@@ -382,6 +384,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleAppliance.OVEN,
|
||||
MieleAppliance.OVEN_MICROWAVE,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MK2,
|
||||
),
|
||||
description=MieleSensorDescription(
|
||||
key="state_core_target_temperature",
|
||||
@@ -398,6 +401,29 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
),
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(
|
||||
MieleAppliance.WASHING_MACHINE,
|
||||
MieleAppliance.WASHER_DRYER,
|
||||
MieleAppliance.OVEN,
|
||||
MieleAppliance.OVEN_MICROWAVE,
|
||||
MieleAppliance.STEAM_OVEN_MICRO,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MK2,
|
||||
),
|
||||
description=MieleSensorDescription(
|
||||
key="state_target_temperature",
|
||||
translation_key="target_temperature",
|
||||
zone=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=(
|
||||
lambda value: cast(int, value.state_target_temperature[0].temperature)
|
||||
/ 100.0
|
||||
),
|
||||
),
|
||||
),
|
||||
MieleSensorDefinition(
|
||||
types=(
|
||||
MieleAppliance.OVEN,
|
||||
|
||||
@@ -876,6 +876,9 @@
|
||||
"core_temperature": {
|
||||
"name": "Core temperature"
|
||||
},
|
||||
"target_temperature": {
|
||||
"name": "Target temperature"
|
||||
},
|
||||
"core_target_temperature": {
|
||||
"name": "Core target temperature"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ from .const import (
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ from .const import DOMAIN, PROCESS_ACTION, PROGRAM_ID, MieleActions, MieleApplia
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The following const classes define program speeds and programs for the vacuum cleaner.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"sections": {
|
||||
"auth": {
|
||||
"name": "Authentication",
|
||||
"description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password.",
|
||||
"description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password. Home Assistant will automatically generate an access token to authenticate with ntfy.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
|
||||
@@ -32,6 +32,8 @@ from .const import (
|
||||
PLACEHOLDER_WEBHOOK_URL,
|
||||
)
|
||||
|
||||
AUTH_TOKEN_URL = "https://intercom.help/plaato/en/articles/5004720-auth_token"
|
||||
|
||||
|
||||
class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handles a Plaato config flow."""
|
||||
@@ -153,7 +155,10 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="api_method",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
|
||||
description_placeholders={
|
||||
PLACEHOLDER_DEVICE_TYPE: device_type.name,
|
||||
"auth_token_url": AUTH_TOKEN_URL,
|
||||
},
|
||||
)
|
||||
|
||||
async def _get_webhook_id(self):
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"api_method": {
|
||||
"title": "Select API method",
|
||||
"description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Selected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank",
|
||||
"description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions]({auth_token_url})\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank",
|
||||
"data": {
|
||||
"use_webhook": "Use webhook",
|
||||
"token": "Auth token"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.2.1"]
|
||||
"requirements": ["ical==9.2.2"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ from homeassistant.const import (
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_BLUETOOTH,
|
||||
CONNECTION_NETWORK_MAC,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .bluetooth import async_connect_scanner
|
||||
@@ -160,6 +164,11 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
"""Sleep period of the device."""
|
||||
return self.config_entry.data.get(CONF_SLEEP_PERIOD, 0)
|
||||
|
||||
@property
|
||||
def connections(self) -> set[tuple[str, str]]:
|
||||
"""Connections of the device."""
|
||||
return {(CONNECTION_NETWORK_MAC, self.mac)}
|
||||
|
||||
def async_setup(self, pending_platforms: list[Platform] | None = None) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self._pending_platforms = pending_platforms
|
||||
@@ -167,7 +176,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
device_entry = dev_reg.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
name=self.name,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.mac)},
|
||||
connections=self.connections,
|
||||
identifiers={(DOMAIN, self.mac)},
|
||||
manufacturer="Shelly",
|
||||
model=get_shelly_model_name(self.model, self.sleep_period, self.device),
|
||||
@@ -523,6 +532,14 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
"""
|
||||
return format_mac(bluetooth_mac_from_primary_mac(self.mac)).upper()
|
||||
|
||||
@property
|
||||
def connections(self) -> set[tuple[str, str]]:
|
||||
"""Connections of the device."""
|
||||
connections = super().connections
|
||||
if not self.sleep_period:
|
||||
connections.add((CONNECTION_BLUETOOTH, self.bluetooth_source))
|
||||
return connections
|
||||
|
||||
async def async_device_online(self, source: str) -> None:
|
||||
"""Handle device going online."""
|
||||
if not self.sleep_period:
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
|
||||
"description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password-protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password-protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Update configuration for {device_name}.\n\nBefore setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.",
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Offer sun based automation rules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_CONDITION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import (
|
||||
ConditionCheckerType,
|
||||
condition_trace_set_result,
|
||||
condition_trace_update_result,
|
||||
trace_condition_function,
|
||||
)
|
||||
from homeassistant.helpers.sun import get_astral_event_date
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
CONDITION_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
**cv.CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): "sun",
|
||||
vol.Optional("before"): cv.sun_event,
|
||||
vol.Optional("before_offset"): cv.time_period,
|
||||
vol.Optional("after"): vol.All(
|
||||
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
),
|
||||
vol.Optional("after_offset"): cv.time_period,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("before", "after"),
|
||||
)
|
||||
|
||||
|
||||
def sun(
|
||||
hass: HomeAssistant,
|
||||
before: str | None = None,
|
||||
after: str | None = None,
|
||||
before_offset: timedelta | None = None,
|
||||
after_offset: timedelta | None = None,
|
||||
) -> bool:
|
||||
"""Test if current time matches sun requirements."""
|
||||
utcnow = dt_util.utcnow()
|
||||
today = dt_util.as_local(utcnow).date()
|
||||
before_offset = before_offset or timedelta(0)
|
||||
after_offset = after_offset or timedelta(0)
|
||||
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
|
||||
|
||||
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
|
||||
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
|
||||
|
||||
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
|
||||
if after_sunrise and has_sunrise_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
|
||||
|
||||
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
|
||||
if after_sunset and has_sunset_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
|
||||
|
||||
# Special case: before sunrise OR after sunset
|
||||
# This will handle the very rare case in the polar region when the sun rises/sets
|
||||
# but does not set/rise.
|
||||
# However this entire condition does not handle those full days of darkness
|
||||
# or light, the following should be used instead:
|
||||
#
|
||||
# condition:
|
||||
# condition: state
|
||||
# entity_id: sun.sun
|
||||
# state: 'above_horizon' (or 'below_horizon')
|
||||
#
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
if sunrise is None and has_sunrise_condition:
|
||||
# There is no sunrise today
|
||||
condition_trace_set_result(False, message="no sunrise today")
|
||||
return False
|
||||
|
||||
if sunset is None and has_sunset_condition:
|
||||
# There is no sunset today
|
||||
condition_trace_set_result(False, message="no sunset today")
|
||||
return False
|
||||
|
||||
if before == SUN_EVENT_SUNRISE:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
if utcnow > wanted_time_before:
|
||||
return False
|
||||
|
||||
if before == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunset) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
if utcnow > wanted_time_before:
|
||||
return False
|
||||
|
||||
if after == SUN_EVENT_SUNRISE:
|
||||
wanted_time_after = cast(datetime, sunrise) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
if utcnow < wanted_time_after:
|
||||
return False
|
||||
|
||||
if after == SUN_EVENT_SUNSET:
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
if utcnow < wanted_time_after:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
|
||||
"""Wrap action method with sun based condition."""
|
||||
before = config.get("before")
|
||||
after = config.get("after")
|
||||
before_offset = config.get("before_offset")
|
||||
after_offset = config.get("after_offset")
|
||||
|
||||
@trace_condition_function
|
||||
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
||||
"""Validate time based if-condition."""
|
||||
return sun(hass, before, after, before_offset, after_offset)
|
||||
|
||||
return sun_if
|
||||
@@ -469,6 +469,7 @@ class ResultStream:
|
||||
use_file_cache: bool
|
||||
language: str
|
||||
options: dict
|
||||
supports_streaming_input: bool
|
||||
|
||||
_manager: SpeechManager
|
||||
|
||||
@@ -484,7 +485,10 @@ class ResultStream:
|
||||
|
||||
@callback
|
||||
def async_set_message(self, message: str) -> None:
|
||||
"""Set message to be generated."""
|
||||
"""Set message to be generated.
|
||||
|
||||
This method will leverage a disk cache to speed up generation.
|
||||
"""
|
||||
self._result_cache.set_result(
|
||||
self._manager.async_cache_message_in_memory(
|
||||
engine=self.engine,
|
||||
@@ -497,7 +501,10 @@ class ResultStream:
|
||||
|
||||
@callback
|
||||
def async_set_message_stream(self, message_stream: AsyncGenerator[str]) -> None:
|
||||
"""Set a stream that will generate the message."""
|
||||
"""Set a stream that will generate the message.
|
||||
|
||||
This method can result in faster first byte when generating long responses.
|
||||
"""
|
||||
self._result_cache.set_result(
|
||||
self._manager.async_cache_message_stream_in_memory(
|
||||
engine=self.engine,
|
||||
@@ -726,6 +733,10 @@ class SpeechManager:
|
||||
if (engine_instance := get_engine_instance(self.hass, engine)) is None:
|
||||
raise HomeAssistantError(f"Provider {engine} not found")
|
||||
|
||||
supports_streaming_input = (
|
||||
isinstance(engine_instance, TextToSpeechEntity)
|
||||
and engine_instance.async_supports_streaming_input()
|
||||
)
|
||||
language, options = self.process_options(engine_instance, language, options)
|
||||
if use_file_cache is None:
|
||||
use_file_cache = self.use_file_cache
|
||||
@@ -741,6 +752,7 @@ class SpeechManager:
|
||||
engine=engine,
|
||||
language=language,
|
||||
options=options,
|
||||
supports_streaming_input=supports_streaming_input,
|
||||
_manager=self,
|
||||
)
|
||||
self.token_to_stream[token] = result_stream
|
||||
|
||||
@@ -89,6 +89,13 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH
|
||||
"""Return a mapping with the default options."""
|
||||
return self._attr_default_options
|
||||
|
||||
@classmethod
|
||||
def async_supports_streaming_input(cls) -> bool:
|
||||
"""Return if the TTS engine supports streaming input."""
|
||||
return (
|
||||
cls.async_stream_tts_audio is not TextToSpeechEntity.async_stream_tts_audio
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
|
||||
"""Return a list of supported voices for a language."""
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"tls": "Enable this if you use a secure connection to your Velbus interface, like a Signum.",
|
||||
"host": "The IP address or hostname of the Velbus interface.",
|
||||
"port": "The port number of the Velbus interface.",
|
||||
"password": "The password of the Velbus interface, this is only needed if the interface is password protected."
|
||||
"password": "The password of the Velbus interface, this is only needed if the interface is password-protected."
|
||||
},
|
||||
"description": "TCP/IP configuration, in case you use a Signum, VelServ, velbus-tcp or any other Velbus to TCP/IP interface."
|
||||
},
|
||||
@@ -58,7 +58,7 @@
|
||||
"services": {
|
||||
"sync_clock": {
|
||||
"name": "Sync clock",
|
||||
"description": "Syncs the Velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.",
|
||||
"description": "Syncs the clock of the Velbus modules to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.",
|
||||
"fields": {
|
||||
"interface": {
|
||||
"name": "Interface",
|
||||
@@ -104,7 +104,7 @@
|
||||
},
|
||||
"set_memo_text": {
|
||||
"name": "Set memo text",
|
||||
"description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text.",
|
||||
"description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD. Be sure the pages of the modules are configured to display the memo text.",
|
||||
"fields": {
|
||||
"interface": {
|
||||
"name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
|
||||
|
||||
@@ -278,6 +278,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# and we'll handle the clean up below.
|
||||
await driver_events.setup(driver)
|
||||
|
||||
if (old_unique_id := entry.unique_id) is not None and old_unique_id != (
|
||||
new_unique_id := str(driver.controller.home_id)
|
||||
):
|
||||
device_registry = dr.async_get(hass)
|
||||
controller_model = "Unknown model"
|
||||
if (
|
||||
(own_node := driver.controller.own_node)
|
||||
and (
|
||||
controller_device_entry := device_registry.async_get_device(
|
||||
identifiers={get_device_id(driver, own_node)}
|
||||
)
|
||||
)
|
||||
and (model := controller_device_entry.model)
|
||||
):
|
||||
controller_model = model
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"migrate_unique_id.{entry.entry_id}",
|
||||
data={
|
||||
"config_entry_id": entry.entry_id,
|
||||
"config_entry_title": entry.title,
|
||||
"controller_model": controller_model,
|
||||
"new_unique_id": new_unique_id,
|
||||
"old_unique_id": old_unique_id,
|
||||
},
|
||||
is_fixable=True,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="migrate_unique_id",
|
||||
)
|
||||
else:
|
||||
async_delete_issue(hass, DOMAIN, f"migrate_unique_id.{entry.entry_id}")
|
||||
|
||||
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
||||
if listen_task.done():
|
||||
listen_error, error_message = _get_listen_task_error(listen_task)
|
||||
|
||||
@@ -57,6 +57,47 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
|
||||
)
|
||||
|
||||
|
||||
class MigrateUniqueIDFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
def __init__(self, data: dict[str, str]) -> None:
|
||||
"""Initialize."""
|
||||
self.description_placeholders: dict[str, str] = {
|
||||
"config_entry_title": data["config_entry_title"],
|
||||
"controller_model": data["controller_model"],
|
||||
"new_unique_id": data["new_unique_id"],
|
||||
"old_unique_id": data["old_unique_id"],
|
||||
}
|
||||
self._config_entry_id: str = data["config_entry_id"]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None:
|
||||
config_entry = self.hass.config_entries.async_get_entry(
|
||||
self._config_entry_id
|
||||
)
|
||||
# If config entry was removed, we can ignore the issue.
|
||||
if config_entry is not None:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
unique_id=self.description_placeholders["new_unique_id"],
|
||||
)
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders=self.description_placeholders,
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
||||
) -> RepairsFlow:
|
||||
@@ -65,4 +106,7 @@ async def async_create_fix_flow(
|
||||
if issue_id.split(".")[0] == "device_config_file_changed":
|
||||
assert data
|
||||
return DeviceConfigFileChangedFlow(data)
|
||||
if issue_id.split(".")[0] == "migrate_unique_id":
|
||||
assert data
|
||||
return MigrateUniqueIDFlow(data)
|
||||
return ConfirmRepairFlow()
|
||||
|
||||
@@ -273,6 +273,17 @@
|
||||
"invalid_server_version": {
|
||||
"description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.",
|
||||
"title": "Newer version of Z-Wave Server needed"
|
||||
},
|
||||
"migrate_unique_id": {
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "A Z-Wave controller of model {controller_model} with a different ID ({new_unique_id}) than the previously connected controller ({old_unique_id}) was connected to the {config_entry_title} configuration entry.\n\nReasons for a different controller ID could be:\n\n1. The controller was factory reset, with a 3rd party application.\n2. A controller Non Volatile Memory (NVM) backup was restored to the controller, with a 3rd party application.\n3. A different controller was connected to this configuration entry.\n\nIf a different controller was connected, you should instead set up a new configuration entry for the new controller.\n\nIf you are sure that the current controller is the correct controller you can confirm this by pressing Submit, and the configuration entry will remember the new controller ID.",
|
||||
"title": "An unknown controller was detected"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "An unknown controller was detected"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -866,17 +866,17 @@ class Config:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .components.frontend import storage as frontend_store
|
||||
|
||||
_, owner_data = await frontend_store.async_user_store(
|
||||
owner_store = await frontend_store.async_user_store(
|
||||
self.hass, owner.id
|
||||
)
|
||||
|
||||
if (
|
||||
"language" in owner_data
|
||||
and "language" in owner_data["language"]
|
||||
"language" in owner_store.data
|
||||
and "language" in owner_store.data["language"]
|
||||
):
|
||||
with suppress(vol.InInvalid):
|
||||
data["language"] = cv.language(
|
||||
owner_data["language"]["language"]
|
||||
owner_store.data["language"]["language"]
|
||||
)
|
||||
# pylint: disable-next=broad-except
|
||||
except Exception:
|
||||
|
||||
@@ -42,8 +42,6 @@ from homeassistant.const import (
|
||||
ENTITY_MATCH_ANY,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
WEEKDAYS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
@@ -60,7 +58,6 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
from . import config_validation as cv, entity_registry as er
|
||||
from .sun import get_astral_event_date
|
||||
from .template import Template, render_complex
|
||||
from .trace import (
|
||||
TraceElement,
|
||||
@@ -85,7 +82,6 @@ _PLATFORM_ALIASES = {
|
||||
"numeric_state": None,
|
||||
"or": None,
|
||||
"state": None,
|
||||
"sun": None,
|
||||
"template": None,
|
||||
"time": None,
|
||||
"trigger": None,
|
||||
@@ -655,105 +651,6 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType:
|
||||
return if_state
|
||||
|
||||
|
||||
def sun(
|
||||
hass: HomeAssistant,
|
||||
before: str | None = None,
|
||||
after: str | None = None,
|
||||
before_offset: timedelta | None = None,
|
||||
after_offset: timedelta | None = None,
|
||||
) -> bool:
|
||||
"""Test if current time matches sun requirements."""
|
||||
utcnow = dt_util.utcnow()
|
||||
today = dt_util.as_local(utcnow).date()
|
||||
before_offset = before_offset or timedelta(0)
|
||||
after_offset = after_offset or timedelta(0)
|
||||
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
|
||||
|
||||
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
|
||||
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
|
||||
|
||||
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
|
||||
if after_sunrise and has_sunrise_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
|
||||
|
||||
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
|
||||
if after_sunset and has_sunset_condition:
|
||||
tomorrow = today + timedelta(days=1)
|
||||
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
|
||||
|
||||
# Special case: before sunrise OR after sunset
|
||||
# This will handle the very rare case in the polar region when the sun rises/sets
|
||||
# but does not set/rise.
|
||||
# However this entire condition does not handle those full days of darkness
|
||||
# or light, the following should be used instead:
|
||||
#
|
||||
# condition:
|
||||
# condition: state
|
||||
# entity_id: sun.sun
|
||||
# state: 'above_horizon' (or 'below_horizon')
|
||||
#
|
||||
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
return utcnow < wanted_time_before or utcnow > wanted_time_after
|
||||
|
||||
if sunrise is None and has_sunrise_condition:
|
||||
# There is no sunrise today
|
||||
condition_trace_set_result(False, message="no sunrise today")
|
||||
return False
|
||||
|
||||
if sunset is None and has_sunset_condition:
|
||||
# There is no sunset today
|
||||
condition_trace_set_result(False, message="no sunset today")
|
||||
return False
|
||||
|
||||
if before == SUN_EVENT_SUNRISE:
|
||||
wanted_time_before = cast(datetime, sunrise) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
if utcnow > wanted_time_before:
|
||||
return False
|
||||
|
||||
if before == SUN_EVENT_SUNSET:
|
||||
wanted_time_before = cast(datetime, sunset) + before_offset
|
||||
condition_trace_update_result(wanted_time_before=wanted_time_before)
|
||||
if utcnow > wanted_time_before:
|
||||
return False
|
||||
|
||||
if after == SUN_EVENT_SUNRISE:
|
||||
wanted_time_after = cast(datetime, sunrise) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
if utcnow < wanted_time_after:
|
||||
return False
|
||||
|
||||
if after == SUN_EVENT_SUNSET:
|
||||
wanted_time_after = cast(datetime, sunset) + after_offset
|
||||
condition_trace_update_result(wanted_time_after=wanted_time_after)
|
||||
if utcnow < wanted_time_after:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def sun_from_config(config: ConfigType) -> ConditionCheckerType:
|
||||
"""Wrap action method with sun based condition."""
|
||||
before = config.get("before")
|
||||
after = config.get("after")
|
||||
before_offset = config.get("before_offset")
|
||||
after_offset = config.get("after_offset")
|
||||
|
||||
@trace_condition_function
|
||||
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
||||
"""Validate time based if-condition."""
|
||||
return sun(hass, before, after, before_offset, after_offset)
|
||||
|
||||
return sun_if
|
||||
|
||||
|
||||
def template(
|
||||
hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None
|
||||
) -> bool:
|
||||
@@ -1054,8 +951,10 @@ async def async_validate_condition_config(
|
||||
return config
|
||||
|
||||
platform = await _async_get_condition_platform(hass, config)
|
||||
if platform is not None and hasattr(platform, "async_validate_condition_config"):
|
||||
return await platform.async_validate_condition_config(hass, config)
|
||||
if platform is not None:
|
||||
if hasattr(platform, "async_validate_condition_config"):
|
||||
return await platform.async_validate_condition_config(hass, config)
|
||||
return cast(ConfigType, platform.CONDITION_SCHEMA(config))
|
||||
if platform is None and condition in ("numeric_state", "state"):
|
||||
validator = cast(
|
||||
Callable[[HomeAssistant, ConfigType], ConfigType],
|
||||
|
||||
@@ -1084,10 +1084,13 @@ def renamed(
|
||||
return validator
|
||||
|
||||
|
||||
type ValueSchemas = dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]]
|
||||
|
||||
|
||||
def key_value_schemas(
|
||||
key: str,
|
||||
value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]],
|
||||
default_schema: VolSchemaType | None = None,
|
||||
value_schemas: ValueSchemas,
|
||||
default_schema: VolSchemaType | Callable[[Any], dict[str, Any]] | None = None,
|
||||
default_description: str | None = None,
|
||||
) -> Callable[[Any], dict[Hashable, Any]]:
|
||||
"""Create a validator that validates based on a value for specific key.
|
||||
@@ -1735,25 +1738,41 @@ CONDITION_SHORTHAND_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
BUILT_IN_CONDITIONS: ValueSchemas = {
|
||||
"and": AND_CONDITION_SCHEMA,
|
||||
"device": DEVICE_CONDITION_SCHEMA,
|
||||
"not": NOT_CONDITION_SCHEMA,
|
||||
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
||||
"or": OR_CONDITION_SCHEMA,
|
||||
"state": STATE_CONDITION_SCHEMA,
|
||||
"template": TEMPLATE_CONDITION_SCHEMA,
|
||||
"time": TIME_CONDITION_SCHEMA,
|
||||
"trigger": TRIGGER_CONDITION_SCHEMA,
|
||||
"zone": ZONE_CONDITION_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
# This is first round of validation, we don't want to mutate the config here already,
|
||||
# just ensure basics as condition type and alias are there.
|
||||
def _base_condition_validator(value: Any) -> Any:
|
||||
vol.Schema(
|
||||
{
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
CONF_CONDITION: vol.All(str, vol.NotIn(BUILT_IN_CONDITIONS)),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)(value)
|
||||
return value
|
||||
|
||||
|
||||
CONDITION_SCHEMA: vol.Schema = vol.Schema(
|
||||
vol.Any(
|
||||
vol.All(
|
||||
expand_condition_shorthand,
|
||||
key_value_schemas(
|
||||
CONF_CONDITION,
|
||||
{
|
||||
"and": AND_CONDITION_SCHEMA,
|
||||
"device": DEVICE_CONDITION_SCHEMA,
|
||||
"not": NOT_CONDITION_SCHEMA,
|
||||
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
||||
"or": OR_CONDITION_SCHEMA,
|
||||
"state": STATE_CONDITION_SCHEMA,
|
||||
"sun": SUN_CONDITION_SCHEMA,
|
||||
"template": TEMPLATE_CONDITION_SCHEMA,
|
||||
"time": TIME_CONDITION_SCHEMA,
|
||||
"trigger": TRIGGER_CONDITION_SCHEMA,
|
||||
"zone": ZONE_CONDITION_SCHEMA,
|
||||
},
|
||||
BUILT_IN_CONDITIONS,
|
||||
_base_condition_validator,
|
||||
),
|
||||
),
|
||||
dynamic_template_condition,
|
||||
@@ -1780,20 +1799,11 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
|
||||
expand_condition_shorthand,
|
||||
key_value_schemas(
|
||||
CONF_CONDITION,
|
||||
{
|
||||
"and": AND_CONDITION_SCHEMA,
|
||||
"device": DEVICE_CONDITION_SCHEMA,
|
||||
"not": NOT_CONDITION_SCHEMA,
|
||||
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
||||
"or": OR_CONDITION_SCHEMA,
|
||||
"state": STATE_CONDITION_SCHEMA,
|
||||
"sun": SUN_CONDITION_SCHEMA,
|
||||
"template": TEMPLATE_CONDITION_SCHEMA,
|
||||
"time": TIME_CONDITION_SCHEMA,
|
||||
"trigger": TRIGGER_CONDITION_SCHEMA,
|
||||
"zone": ZONE_CONDITION_SCHEMA,
|
||||
},
|
||||
dynamic_template_condition_action,
|
||||
BUILT_IN_CONDITIONS,
|
||||
vol.Any(
|
||||
dynamic_template_condition_action,
|
||||
_base_condition_validator,
|
||||
),
|
||||
"a list of conditions or a valid template",
|
||||
),
|
||||
)
|
||||
@@ -1852,7 +1862,7 @@ def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]:
|
||||
return flatlist
|
||||
|
||||
|
||||
# This is first round of validation, we don't want to process the config here already,
|
||||
# This is first round of validation, we don't want to mutate the config here already,
|
||||
# just ensure basics as platform and ID are there.
|
||||
def _base_trigger_validator(value: Any) -> Any:
|
||||
_base_trigger_validator_schema(value)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.1.1
|
||||
aiodhcpwatcher==1.2.0
|
||||
aiodiscover==2.7.0
|
||||
aiodns==3.4.0
|
||||
aiohasupervisor==0.3.1
|
||||
|
||||
Generated
+5
-5
@@ -211,10 +211,10 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.12.0
|
||||
aiocomelit==0.12.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.1.1
|
||||
aiodhcpwatcher==1.2.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==2.7.0
|
||||
@@ -983,7 +983,7 @@ gardena-bluetooth==1.6.0
|
||||
gassist-text==0.0.12
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==7.0.0
|
||||
gcal-sync==7.0.1
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.7.1
|
||||
@@ -1197,7 +1197,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.2.1
|
||||
ical==9.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -3101,7 +3101,7 @@ xbox-webapi==2.1.0
|
||||
xiaomi-ble==0.38.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.6.0
|
||||
xknx==3.8.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.8.2
|
||||
|
||||
@@ -18,7 +18,7 @@ pre-commit==4.0.0
|
||||
pydantic==2.11.3
|
||||
pylint==3.3.7
|
||||
pylint-per-file-ignores==1.4.0
|
||||
pipdeptree==2.25.1
|
||||
pipdeptree==2.26.1
|
||||
pytest-asyncio==0.26.0
|
||||
pytest-aiohttp==1.1.0
|
||||
pytest-cov==6.0.0
|
||||
|
||||
Generated
+5
-5
@@ -199,10 +199,10 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.12.0
|
||||
aiocomelit==0.12.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.1.1
|
||||
aiodhcpwatcher==1.2.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==2.7.0
|
||||
@@ -837,7 +837,7 @@ gardena-bluetooth==1.6.0
|
||||
gassist-text==0.0.12
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==7.0.0
|
||||
gcal-sync==7.0.1
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.7.1
|
||||
@@ -1018,7 +1018,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.2.1
|
||||
ical==9.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -2509,7 +2509,7 @@ xbox-webapi==2.1.0
|
||||
xiaomi-ble==0.38.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.6.0
|
||||
xknx==3.8.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.8.2
|
||||
|
||||
Generated
+1
-1
@@ -24,7 +24,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
|
||||
--no-cache \
|
||||
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
||||
-r /usr/src/homeassistant/requirements.txt \
|
||||
stdlib-list==0.10.0 pipdeptree==2.25.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.2 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
|
||||
|
||||
LABEL "name"="hassfest"
|
||||
|
||||
@@ -37,7 +37,7 @@ from tests.common import (
|
||||
mock_platform,
|
||||
)
|
||||
from tests.components.stt.common import MockSTTProvider, MockSTTProviderEntity
|
||||
from tests.components.tts.common import MockTTSProvider
|
||||
from tests.components.tts.common import MockTTSEntity, MockTTSProvider
|
||||
|
||||
_TRANSCRIPT = "test transcript"
|
||||
|
||||
@@ -68,6 +68,15 @@ async def mock_tts_provider() -> MockTTSProvider:
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tts_entity() -> MockTTSEntity:
|
||||
"""Test TTS entity."""
|
||||
entity = MockTTSEntity("en")
|
||||
entity._attr_unique_id = "test_tts"
|
||||
entity._attr_supported_languages = ["en-US"]
|
||||
return entity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_stt_provider() -> MockSTTProvider:
|
||||
"""Mock STT provider."""
|
||||
@@ -198,6 +207,7 @@ async def init_supporting_components(
|
||||
mock_stt_provider: MockSTTProvider,
|
||||
mock_stt_provider_entity: MockSTTProviderEntity,
|
||||
mock_tts_provider: MockTTSProvider,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
mock_wake_word_provider_entity: MockWakeWordEntity,
|
||||
mock_wake_word_provider_entity2: MockWakeWordEntity2,
|
||||
config_flow_fixture,
|
||||
@@ -209,7 +219,7 @@ async def init_supporting_components(
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [Platform.STT, Platform.WAKE_WORD]
|
||||
config_entry, [Platform.STT, Platform.TTS, Platform.WAKE_WORD]
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -230,6 +240,14 @@ async def init_supporting_components(
|
||||
"""Set up test stt platform via config entry."""
|
||||
async_add_entities([mock_stt_provider_entity])
|
||||
|
||||
async def async_setup_entry_tts_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up test tts platform via config entry."""
|
||||
async_add_entities([mock_tts_entity])
|
||||
|
||||
async def async_setup_entry_wake_word_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -253,6 +271,7 @@ async def init_supporting_components(
|
||||
"test.tts",
|
||||
MockTTSPlatform(
|
||||
async_get_engine=AsyncMock(return_value=mock_tts_provider),
|
||||
async_setup_entry=async_setup_entry_tts_platform,
|
||||
),
|
||||
)
|
||||
mock_platform(
|
||||
|
||||
@@ -74,17 +74,17 @@
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
}),
|
||||
'type': <PipelineEventType.TTS_START: 'tts-start'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
@@ -395,17 +395,17 @@
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
}),
|
||||
'type': <PipelineEventType.TTS_START: 'tts-start'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
|
||||
@@ -1,4 +1,158 @@
|
||||
# serializer version: 1
|
||||
# name: test_chat_log_tts_streaming[to_stream_tts0]
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
'conversation_id': 'mock-ulid',
|
||||
'language': 'en',
|
||||
'pipeline': <ANY>,
|
||||
'tts_output': dict({
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'mocked-token.mp3',
|
||||
'url': '/api/tts_proxy/mocked-token.mp3',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.RUN_START: 'run-start'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'conversation_id': 'mock-ulid',
|
||||
'device_id': None,
|
||||
'engine': 'test-agent',
|
||||
'intent_input': 'Set a timer',
|
||||
'language': 'en',
|
||||
'prefer_local_intents': False,
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_START: 'intent-start'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'role': 'assistant',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': 'hello,',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': ' ',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': 'how',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': ' ',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': 'are',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': ' ',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': 'you',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'chat_log_delta': dict({
|
||||
'content': '?',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'intent_output': dict({
|
||||
'continue_conversation': True,
|
||||
'conversation_id': <ANY>,
|
||||
'response': dict({
|
||||
'card': dict({
|
||||
}),
|
||||
'data': dict({
|
||||
'failed': list([
|
||||
]),
|
||||
'success': list([
|
||||
]),
|
||||
'targets': list([
|
||||
]),
|
||||
}),
|
||||
'language': 'en',
|
||||
'response_type': 'action_done',
|
||||
'speech': dict({
|
||||
'plain': dict({
|
||||
'extra_data': None,
|
||||
'speech': 'hello, how are you?',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'processed_locally': False,
|
||||
}),
|
||||
'type': <PipelineEventType.INTENT_END: 'intent-end'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': 'hello, how are you?',
|
||||
'voice': None,
|
||||
}),
|
||||
'type': <PipelineEventType.TTS_START: 'tts-start'>,
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'tts_output': dict({
|
||||
'media_id': 'media-source://tts/tts.test?message=hello,+how+are+you?&language=en_US&tts_options=%7B%7D',
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'mocked-token.mp3',
|
||||
'url': '/api/tts_proxy/mocked-token.mp3',
|
||||
}),
|
||||
}),
|
||||
'type': <PipelineEventType.TTS_END: 'tts-end'>,
|
||||
}),
|
||||
dict({
|
||||
'data': None,
|
||||
'type': <PipelineEventType.RUN_END: 'run-end'>,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_pipeline_language_used_instead_of_conversation_language
|
||||
list([
|
||||
dict({
|
||||
|
||||
@@ -71,16 +71,16 @@
|
||||
# ---
|
||||
# name: test_audio_pipeline.5
|
||||
dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_audio_pipeline.6
|
||||
dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
@@ -162,16 +162,16 @@
|
||||
# ---
|
||||
# name: test_audio_pipeline_debug.5
|
||||
dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_audio_pipeline_debug.6
|
||||
dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
@@ -265,16 +265,16 @@
|
||||
# ---
|
||||
# name: test_audio_pipeline_with_enhancements.5
|
||||
dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_audio_pipeline_with_enhancements.6
|
||||
dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
@@ -378,16 +378,16 @@
|
||||
# ---
|
||||
# name: test_audio_pipeline_with_wake_word_no_timeout.7
|
||||
dict({
|
||||
'engine': 'test',
|
||||
'language': 'en-US',
|
||||
'engine': 'tts.test',
|
||||
'language': 'en_US',
|
||||
'tts_input': "Sorry, I couldn't understand that",
|
||||
'voice': 'james_earl_jones',
|
||||
'voice': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_audio_pipeline_with_wake_word_no_timeout.8
|
||||
dict({
|
||||
'tts_output': dict({
|
||||
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
|
||||
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
|
||||
'mime_type': 'audio/mpeg',
|
||||
'token': 'test_token.mp3',
|
||||
'url': '/api/tts_proxy/test_token.mp3',
|
||||
|
||||
@@ -40,6 +40,7 @@ from . import MANY_LANGUAGES, process_events
|
||||
from .conftest import (
|
||||
MockSTTProvider,
|
||||
MockSTTProviderEntity,
|
||||
MockTTSEntity,
|
||||
MockTTSProvider,
|
||||
MockWakeWordEntity,
|
||||
make_10ms_chunk,
|
||||
@@ -62,6 +63,12 @@ async def load_homeassistant(hass: HomeAssistant) -> None:
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def disable_tts_entity(mock_tts_entity: tts.TextToSpeechEntity) -> None:
|
||||
"""Disable the TTS entity."""
|
||||
mock_tts_entity._attr_entity_registry_enabled_default = False
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_components")
|
||||
async def test_load_pipelines(hass: HomeAssistant) -> None:
|
||||
"""Make sure that we can load/save data correctly."""
|
||||
@@ -283,6 +290,7 @@ async def test_migrate_pipeline_store(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_supporting_components")
|
||||
@pytest.mark.usefixtures("disable_tts_entity")
|
||||
async def test_create_default_pipeline(hass: HomeAssistant) -> None:
|
||||
"""Test async_create_default_pipeline."""
|
||||
assert await async_setup_component(hass, "assist_pipeline", {})
|
||||
@@ -430,6 +438,7 @@ async def test_default_pipeline_no_stt_tts(
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_supporting_components")
|
||||
@pytest.mark.usefixtures("disable_tts_entity")
|
||||
async def test_default_pipeline(
|
||||
hass: HomeAssistant,
|
||||
mock_stt_provider_entity: MockSTTProviderEntity,
|
||||
@@ -474,6 +483,7 @@ async def test_default_pipeline(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_supporting_components")
|
||||
@pytest.mark.usefixtures("disable_tts_entity")
|
||||
async def test_default_pipeline_unsupported_stt_language(
|
||||
hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity
|
||||
) -> None:
|
||||
@@ -504,6 +514,7 @@ async def test_default_pipeline_unsupported_stt_language(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_supporting_components")
|
||||
@pytest.mark.usefixtures("disable_tts_entity")
|
||||
async def test_default_pipeline_unsupported_tts_language(
|
||||
hass: HomeAssistant, mock_tts_provider: MockTTSProvider
|
||||
) -> None:
|
||||
@@ -825,7 +836,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None:
|
||||
async def test_tts_audio_output(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_tts_provider: MockTTSProvider,
|
||||
mock_tts_entity: MockTTSProvider,
|
||||
init_components,
|
||||
pipeline_data: assist_pipeline.pipeline.PipelineData,
|
||||
mock_chat_session: chat_session.ChatSession,
|
||||
@@ -869,7 +880,7 @@ async def test_tts_audio_output(
|
||||
== 1
|
||||
)
|
||||
|
||||
with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio:
|
||||
with patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio:
|
||||
await pipeline_input.execute()
|
||||
|
||||
for event in events:
|
||||
@@ -881,14 +892,14 @@ async def test_tts_audio_output(
|
||||
# Ensure that no unsupported options were passed in
|
||||
assert mock_get_tts_audio.called
|
||||
options = mock_get_tts_audio.call_args_list[0].kwargs["options"]
|
||||
extra_options = set(options).difference(mock_tts_provider.supported_options)
|
||||
extra_options = set(options).difference(mock_tts_entity.supported_options)
|
||||
assert len(extra_options) == 0, extra_options
|
||||
|
||||
|
||||
async def test_tts_wav_preferred_format(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_tts_provider: MockTTSProvider,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
init_components,
|
||||
mock_chat_session: chat_session.ChatSession,
|
||||
pipeline_data: assist_pipeline.pipeline.PipelineData,
|
||||
@@ -920,7 +931,7 @@ async def test_tts_wav_preferred_format(
|
||||
await pipeline_input.validate()
|
||||
|
||||
# Make the TTS provider support preferred format options
|
||||
supported_options = list(mock_tts_provider.supported_options or [])
|
||||
supported_options = list(mock_tts_entity.supported_options or [])
|
||||
supported_options.extend(
|
||||
[
|
||||
tts.ATTR_PREFERRED_FORMAT,
|
||||
@@ -931,8 +942,8 @@ async def test_tts_wav_preferred_format(
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_tts_provider, "_supported_options", supported_options),
|
||||
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
|
||||
patch.object(mock_tts_entity, "_supported_options", supported_options),
|
||||
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
|
||||
):
|
||||
await pipeline_input.execute()
|
||||
|
||||
@@ -955,7 +966,7 @@ async def test_tts_wav_preferred_format(
|
||||
async def test_tts_dict_preferred_format(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_tts_provider: MockTTSProvider,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
init_components,
|
||||
mock_chat_session: chat_session.ChatSession,
|
||||
pipeline_data: assist_pipeline.pipeline.PipelineData,
|
||||
@@ -992,7 +1003,7 @@ async def test_tts_dict_preferred_format(
|
||||
await pipeline_input.validate()
|
||||
|
||||
# Make the TTS provider support preferred format options
|
||||
supported_options = list(mock_tts_provider.supported_options or [])
|
||||
supported_options = list(mock_tts_entity.supported_options or [])
|
||||
supported_options.extend(
|
||||
[
|
||||
tts.ATTR_PREFERRED_FORMAT,
|
||||
@@ -1003,8 +1014,8 @@ async def test_tts_dict_preferred_format(
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_tts_provider, "_supported_options", supported_options),
|
||||
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
|
||||
patch.object(mock_tts_entity, "_supported_options", supported_options),
|
||||
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
|
||||
):
|
||||
await pipeline_input.execute()
|
||||
|
||||
@@ -1545,3 +1556,143 @@ async def test_pipeline_language_used_instead_of_conversation_language(
|
||||
mock_async_converse.call_args_list[0].kwargs.get("language")
|
||||
== pipeline.language
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"to_stream_tts",
|
||||
[
|
||||
[
|
||||
"hello,",
|
||||
" ",
|
||||
"how",
|
||||
" ",
|
||||
"are",
|
||||
" ",
|
||||
"you",
|
||||
"?",
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_chat_log_tts_streaming(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
init_components,
|
||||
mock_chat_session: chat_session.ChatSession,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
pipeline_data: assist_pipeline.pipeline.PipelineData,
|
||||
to_stream_tts: list[str],
|
||||
) -> None:
|
||||
"""Test that chat log events are streamed to the TTS entity."""
|
||||
events: list[assist_pipeline.PipelineEvent] = []
|
||||
|
||||
pipeline_store = pipeline_data.pipeline_store
|
||||
pipeline_id = pipeline_store.async_get_preferred_item()
|
||||
pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id)
|
||||
await assist_pipeline.pipeline.async_update_pipeline(
|
||||
hass, pipeline, conversation_engine="test-agent"
|
||||
)
|
||||
pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id)
|
||||
|
||||
pipeline_input = assist_pipeline.pipeline.PipelineInput(
|
||||
intent_input="Set a timer",
|
||||
session=mock_chat_session,
|
||||
run=assist_pipeline.pipeline.PipelineRun(
|
||||
hass,
|
||||
context=Context(),
|
||||
pipeline=pipeline,
|
||||
start_stage=assist_pipeline.PipelineStage.INTENT,
|
||||
end_stage=assist_pipeline.PipelineStage.TTS,
|
||||
event_callback=events.append,
|
||||
),
|
||||
)
|
||||
|
||||
received_tts = []
|
||||
|
||||
async def async_stream_tts_audio(
|
||||
request: tts.TTSAudioRequest,
|
||||
) -> tts.TTSAudioResponse:
|
||||
"""Mock stream TTS audio."""
|
||||
|
||||
async def gen_data():
|
||||
async for msg in request.message_gen:
|
||||
received_tts.append(msg)
|
||||
yield msg.encode()
|
||||
|
||||
return tts.TTSAudioResponse(
|
||||
extension="mp3",
|
||||
data_gen=gen_data(),
|
||||
)
|
||||
|
||||
mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info",
|
||||
return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"),
|
||||
):
|
||||
await pipeline_input.validate()
|
||||
|
||||
async def mock_converse(
|
||||
hass: HomeAssistant,
|
||||
text: str,
|
||||
conversation_id: str | None,
|
||||
context: Context,
|
||||
language: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
device_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
):
|
||||
"""Mock converse."""
|
||||
conversation_input = conversation.ConversationInput(
|
||||
text=text,
|
||||
context=context,
|
||||
conversation_id=conversation_id,
|
||||
device_id=device_id,
|
||||
language=language,
|
||||
agent_id=agent_id,
|
||||
extra_system_prompt=extra_system_prompt,
|
||||
)
|
||||
|
||||
async def stream_llm_response():
|
||||
yield {"role": "assistant"}
|
||||
for chunk in to_stream_tts:
|
||||
yield {"content": chunk}
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass, conversation_id) as session,
|
||||
conversation.async_get_chat_log(
|
||||
hass,
|
||||
session,
|
||||
conversation_input,
|
||||
) as chat_log,
|
||||
):
|
||||
async for _content in chat_log.async_add_delta_content_stream(
|
||||
agent_id, stream_llm_response()
|
||||
):
|
||||
pass
|
||||
intent_response = intent.IntentResponse(language)
|
||||
intent_response.async_set_speech("".join(to_stream_tts))
|
||||
return conversation.ConversationResult(
|
||||
response=intent_response,
|
||||
conversation_id=chat_log.conversation_id,
|
||||
continue_conversation=chat_log.continue_conversation,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.assist_pipeline.pipeline.conversation.async_converse",
|
||||
mock_converse,
|
||||
):
|
||||
await pipeline_input.execute()
|
||||
|
||||
stream = tts.async_get_stream(hass, events[0].data["tts_output"]["token"])
|
||||
assert stream is not None
|
||||
tts_result = "".join(
|
||||
[chunk.decode() async for chunk in stream.async_stream_result()]
|
||||
)
|
||||
|
||||
streamed_text = "".join(to_stream_tts)
|
||||
assert tts_result == streamed_text
|
||||
assert len(received_tts) == 1
|
||||
assert "".join(received_tts) == streamed_text
|
||||
|
||||
assert process_events(events) == snapshot
|
||||
|
||||
@@ -1153,9 +1153,9 @@ async def test_get_pipeline(
|
||||
"name": "Home Assistant",
|
||||
"stt_engine": "stt.mock_stt",
|
||||
"stt_language": "en-US",
|
||||
"tts_engine": "test",
|
||||
"tts_language": "en-US",
|
||||
"tts_voice": "james_earl_jones",
|
||||
"tts_engine": "tts.test",
|
||||
"tts_language": "en_US",
|
||||
"tts_voice": None,
|
||||
"wake_word_entity": None,
|
||||
"wake_word_id": None,
|
||||
"prefer_local_intents": False,
|
||||
@@ -1179,9 +1179,9 @@ async def test_get_pipeline(
|
||||
# It found these defaults
|
||||
"stt_engine": "stt.mock_stt",
|
||||
"stt_language": "en-US",
|
||||
"tts_engine": "test",
|
||||
"tts_language": "en-US",
|
||||
"tts_voice": "james_earl_jones",
|
||||
"tts_engine": "tts.test",
|
||||
"tts_language": "en_US",
|
||||
"tts_voice": None,
|
||||
"wake_word_entity": None,
|
||||
"wake_word_id": None,
|
||||
"prefer_local_intents": False,
|
||||
@@ -1266,9 +1266,9 @@ async def test_list_pipelines(
|
||||
"name": "Home Assistant",
|
||||
"stt_engine": "stt.mock_stt",
|
||||
"stt_language": "en-US",
|
||||
"tts_engine": "test",
|
||||
"tts_language": "en-US",
|
||||
"tts_voice": "james_earl_jones",
|
||||
"tts_engine": "tts.test",
|
||||
"tts_language": "en_US",
|
||||
"tts_voice": None,
|
||||
"wake_word_entity": None,
|
||||
"wake_word_id": None,
|
||||
"prefer_local_intents": False,
|
||||
|
||||
@@ -165,13 +165,15 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
async def stream_source(self) -> str | None:
|
||||
return STREAM_SOURCE
|
||||
|
||||
class SyncCamera(BaseCamera):
|
||||
"""Mock Camera with native sync WebRTC support."""
|
||||
class AsyncNoCandidateCamera(BaseCamera):
|
||||
"""Mock Camera with native async WebRTC support but not implemented candidate support."""
|
||||
|
||||
_attr_name = "Sync"
|
||||
_attr_name = "Async No Candidate"
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
return WEBRTC_ANSWER
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
|
||||
) -> None:
|
||||
send_message(WebRTCAnswer(WEBRTC_ANSWER))
|
||||
|
||||
class AsyncCamera(BaseCamera):
|
||||
"""Mock Camera with native async WebRTC support."""
|
||||
@@ -221,7 +223,10 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
),
|
||||
)
|
||||
setup_test_component_platform(
|
||||
hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True
|
||||
hass,
|
||||
camera.DOMAIN,
|
||||
[AsyncNoCandidateCamera(), AsyncCamera()],
|
||||
from_config_entry=True,
|
||||
)
|
||||
mock_platform(hass, f"{domain}.config_flow", Mock())
|
||||
|
||||
|
||||
@@ -968,24 +968,19 @@ async def test_camera_capabilities_webrtc(
|
||||
"""Test WebRTC camera capabilities."""
|
||||
|
||||
await _test_capabilities(
|
||||
hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
hass, hass_ws_client, "camera.async", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expect_native_async_webrtc"),
|
||||
[("camera.sync", False), ("camera.async", True)],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider")
|
||||
async def test_webrtc_provider_not_added_for_native_webrtc(
|
||||
hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support."""
|
||||
camera_obj = get_camera_from_entity_id(hass, entity_id)
|
||||
camera_obj = get_camera_from_entity_id(hass, "camera.async")
|
||||
assert camera_obj
|
||||
assert camera_obj._webrtc_provider is None
|
||||
assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc
|
||||
assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc
|
||||
assert camera_obj._supports_native_async_webrtc is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
|
||||
@@ -1016,14 +1011,12 @@ async def test_camera_capabilities_changing_non_native_support(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
@pytest.mark.parametrize(("entity_id"), ["camera.sync", "camera.async"])
|
||||
async def test_camera_capabilities_changing_native_support(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test WebRTC camera capabilities."""
|
||||
cam = get_camera_from_entity_id(hass, entity_id)
|
||||
cam = get_camera_from_entity_id(hass, "camera.async")
|
||||
assert cam.supported_features == camera.CameraEntityFeature.STREAM
|
||||
|
||||
await _test_capabilities(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Test camera WebRTC."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
@@ -10,9 +9,7 @@ from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
DATA_ICE_SERVERS,
|
||||
DOMAIN as CAMERA_DOMAIN,
|
||||
Camera,
|
||||
CameraEntityFeature,
|
||||
CameraWebRTCProvider,
|
||||
StreamType,
|
||||
WebRTCAnswer,
|
||||
@@ -25,22 +22,12 @@ from homeassistant.components.camera import (
|
||||
get_camera_from_entity_id,
|
||||
)
|
||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
mock_config_flow,
|
||||
mock_integration,
|
||||
mock_platform,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
WEBRTC_OFFER = "v=0\r\n"
|
||||
@@ -57,84 +44,6 @@ class Go2RTCProvider(SomeTestProvider):
|
||||
return "go2rtc"
|
||||
|
||||
|
||||
class MockCamera(Camera):
|
||||
"""Mock Camera Entity."""
|
||||
|
||||
_attr_name = "Test"
|
||||
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the mock entity."""
|
||||
super().__init__()
|
||||
self._sync_answer: str | None | Exception = WEBRTC_ANSWER
|
||||
|
||||
def set_sync_answer(self, value: str | None | Exception) -> None:
|
||||
"""Set sync offer answer."""
|
||||
self._sync_answer = value
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
"""Handle the WebRTC offer and return the answer."""
|
||||
if isinstance(self._sync_answer, Exception):
|
||||
raise self._sync_answer
|
||||
return self._sync_answer
|
||||
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the source of the stream.
|
||||
|
||||
This is used by cameras with CameraEntityFeature.STREAM
|
||||
and StreamType.HLS.
|
||||
"""
|
||||
return "rtsp://stream"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_test_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> MockCamera:
|
||||
"""Initialize components."""
|
||||
|
||||
entry = MockConfigEntry(domain=TEST_INTEGRATION_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
async def async_setup_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [CAMERA_DOMAIN]
|
||||
)
|
||||
return True
|
||||
|
||||
async def async_unload_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Unload test config entry."""
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, CAMERA_DOMAIN
|
||||
)
|
||||
return True
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
TEST_INTEGRATION_DOMAIN,
|
||||
async_setup_entry=async_setup_entry_init,
|
||||
async_unload_entry=async_unload_entry_init,
|
||||
),
|
||||
)
|
||||
test_camera = MockCamera()
|
||||
setup_test_component_platform(
|
||||
hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True
|
||||
)
|
||||
mock_platform(hass, f"{TEST_INTEGRATION_DOMAIN}.config_flow", Mock())
|
||||
|
||||
with mock_config_flow(TEST_INTEGRATION_DOMAIN, ConfigFlow):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return test_camera
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
|
||||
async def test_async_register_webrtc_provider(
|
||||
hass: HomeAssistant,
|
||||
@@ -302,7 +211,6 @@ async def test_ws_get_client_config(
|
||||
},
|
||||
],
|
||||
},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
@callback
|
||||
@@ -341,30 +249,6 @@ async def test_ws_get_client_config(
|
||||
},
|
||||
],
|
||||
},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_ws_get_client_config_sync_offer(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test get WebRTC client config, when camera is supporting sync offer."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
# Assert WebSocket response
|
||||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"configuration": {},
|
||||
"getCandidatesUpfront": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -391,7 +275,6 @@ async def test_ws_get_client_config_custom_config(
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]},
|
||||
"getCandidatesUpfront": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -625,144 +508,6 @@ async def test_websocket_webrtc_offer_missing_offer(
|
||||
assert response["error"]["code"] == "invalid_format"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error", "expected_message"),
|
||||
[
|
||||
(ValueError("value error"), "value error"),
|
||||
(HomeAssistantError("offer failed"), "offer failed"),
|
||||
(TimeoutError(), "Timeout handling WebRTC offer"),
|
||||
],
|
||||
)
|
||||
async def test_websocket_webrtc_offer_failure(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
init_test_integration: MockCamera,
|
||||
error: Exception,
|
||||
expected_message: str,
|
||||
) -> None:
|
||||
"""Test WebRTC stream that fails handling the offer."""
|
||||
client = await hass_ws_client(hass)
|
||||
init_test_integration.set_sync_answer(error)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.test",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Error
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {
|
||||
"type": "error",
|
||||
"code": "webrtc_offer_failed",
|
||||
"message": expected_message,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_websocket_webrtc_offer_sync(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test sync WebRTC stream offer."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.sync",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert (
|
||||
"tests.components.camera.conftest",
|
||||
logging.WARNING,
|
||||
(
|
||||
"async_handle_web_rtc_offer was called from camera, this is a deprecated "
|
||||
"function which will be removed in HA Core 2025.6. Use "
|
||||
"async_handle_async_webrtc_offer instead"
|
||||
),
|
||||
) in caplog.record_tuples
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Answer
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {"type": "answer", "answer": WEBRTC_ANSWER}
|
||||
|
||||
|
||||
async def test_websocket_webrtc_offer_sync_no_answer(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
init_test_integration: MockCamera,
|
||||
) -> None:
|
||||
"""Test sync WebRTC stream offer with no answer."""
|
||||
client = await hass_ws_client(hass)
|
||||
init_test_integration.set_sync_answer(None)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/offer",
|
||||
"entity_id": "camera.test",
|
||||
"offer": WEBRTC_OFFER,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
subscription_id = response["id"]
|
||||
|
||||
# Session id
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"]["type"] == "session"
|
||||
|
||||
# Answer
|
||||
response = await client.receive_json()
|
||||
assert response["id"] == subscription_id
|
||||
assert response["type"] == "event"
|
||||
assert response["event"] == {
|
||||
"type": "error",
|
||||
"code": "webrtc_offer_failed",
|
||||
"message": "No answer on WebRTC offer",
|
||||
}
|
||||
assert (
|
||||
"homeassistant.components.camera",
|
||||
logging.ERROR,
|
||||
"Error handling WebRTC offer: No answer",
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera")
|
||||
async def test_websocket_webrtc_offer_invalid_stream_type(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
@@ -901,7 +646,7 @@ async def test_ws_webrtc_candidate_not_supported(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.sync",
|
||||
"entity_id": "camera.async_no_candidate",
|
||||
"session_id": "session_id",
|
||||
"candidate": {"candidate": "candidate"},
|
||||
}
|
||||
|
||||
@@ -482,19 +482,20 @@ async def test_async_create_repair_issue_unknown(
|
||||
cloud: MagicMock,
|
||||
mock_cloud_setup: None,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test not creating repair issue for unknown repairs."""
|
||||
identifier = "abc123"
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Invalid translation key unknown_translation_key",
|
||||
):
|
||||
await cloud.client.async_create_repair_issue(
|
||||
identifier=identifier,
|
||||
translation_key="unknown_translation_key",
|
||||
placeholders={"custom_domains": "example.com"},
|
||||
severity="error",
|
||||
)
|
||||
await cloud.client.async_create_repair_issue(
|
||||
identifier=identifier,
|
||||
translation_key="unknown_translation_key",
|
||||
placeholders={"custom_domains": "example.com"},
|
||||
severity="error",
|
||||
)
|
||||
assert (
|
||||
"Invalid translation key unknown_translation_key for repair issue abc123"
|
||||
in caplog.text
|
||||
)
|
||||
issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier)
|
||||
assert issue is None
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.devolo_home_network.const import (
|
||||
CONNECTED_TO_ROUTER,
|
||||
LONG_UPDATE_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.binary_sensor import DOMAIN as PLATFORM
|
||||
from homeassistant.components.devolo_home_network.const import LONG_UPDATE_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -24,19 +22,20 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_binary_sensor_setup(hass: HomeAssistant) -> None:
|
||||
async def test_binary_sensor_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the binary sensor component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}")
|
||||
is None
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_connected_to_router"
|
||||
).disabled
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@@ -50,7 +49,7 @@ async def test_update_attached_to_router(
|
||||
"""Test state change of a attached_to_router binary sensor device."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
state_key = f"{BINARY_SENSOR_DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}"
|
||||
state_key = f"{PLATFORM}.{device_name}_connected_to_router"
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -81,5 +80,3 @@ async def test_update_attached_to_router(
|
||||
state = hass.states.get(state_key)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -8,7 +8,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as PLATFORM, SERVICE_PRESS
|
||||
from homeassistant.components.devolo_home_network.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -19,22 +19,27 @@ from .mock import MockDevice
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_button_setup(hass: HomeAssistant) -> None:
|
||||
async def test_button_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the button component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{PLATFORM}.{device_name}_identify_device_with_a_blinking_led")
|
||||
is not None
|
||||
)
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_start_plc_pairing") is not None
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_restart_device") is not None
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_start_wps") is not None
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_identify_device_with_a_blinking_led"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_start_plc_pairing"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_restart_device"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(f"{PLATFORM}.{device_name}_start_wps").disabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -107,8 +112,6 @@ async def test_button(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None:
|
||||
"""Test setting unautherized triggers the reauth flow."""
|
||||
@@ -139,5 +142,3 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == entry.entry_id
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -303,5 +303,4 @@ async def test_form_reauth(hass: HomeAssistant) -> None:
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
assert result3["reason"] == "reauth_successful"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.data[CONF_PASSWORD] == "test-right-password"
|
||||
|
||||
@@ -70,8 +70,6 @@ async def test_device_tracker(
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_restoring_clients(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -9,7 +9,8 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.devolo_home_network.const import SHORT_UPDATE_INTERVAL
|
||||
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
|
||||
from homeassistant.components.image import DOMAIN as PLATFORM
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -24,21 +25,20 @@ from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_image_setup(hass: HomeAssistant) -> None:
|
||||
async def test_image_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the image component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
hass.states.get(
|
||||
f"{IMAGE_DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_guest_wi_fi_credentials_as_qr_code"
|
||||
).disabled
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00")
|
||||
@@ -53,7 +53,7 @@ async def test_guest_wifi_qr(
|
||||
"""Test showing a QR code of the guest wifi credentials."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
state_key = f"{IMAGE_DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code"
|
||||
state_key = f"{PLATFORM}.{device_name}_guest_wi_fi_credentials_as_qr_code"
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -95,5 +95,3 @@ async def test_guest_wifi_qr(
|
||||
resp = await client.get(f"/api/image_proxy/{state_key}")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert await resp.read() != body
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -27,49 +27,41 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_sensor_setup(hass: HomeAssistant) -> None:
|
||||
async def test_sensor_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the sensor component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{PLATFORM}.{device_name}_connected_wi_fi_clients") is not None
|
||||
)
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_connected_plc_devices") is None
|
||||
assert (
|
||||
hass.states.get(f"{PLATFORM}.{device_name}_neighboring_wi_fi_networks") is None
|
||||
)
|
||||
assert (
|
||||
hass.states.get(
|
||||
f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
hass.states.get(
|
||||
f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[1].user_device_name}"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
hass.states.get(
|
||||
f"{PLATFORM}.{device_name}_plc_downlink_phyrate_{PLCNET.devices[2].user_device_name}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
hass.states.get(
|
||||
f"{PLATFORM}.{device_name}_plc_uplink_phyrate_{PLCNET.devices[2].user_device_name}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{PLATFORM}.{device_name}_last_restart_of_the_device") is None
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_connected_wi_fi_clients"
|
||||
).disabled
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_connected_plc_devices"
|
||||
).disabled
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_neighboring_wi_fi_networks"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[1].user_device_name}"
|
||||
).disabled
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[2].user_device_name}"
|
||||
).disabled
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[2].user_device_name}"
|
||||
).disabled
|
||||
assert entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_last_restart_of_the_device"
|
||||
).disabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -145,8 +137,6 @@ async def test_sensor(
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_update_plc_phyrates(
|
||||
hass: HomeAssistant,
|
||||
@@ -198,8 +188,6 @@ async def test_update_plc_phyrates(
|
||||
assert state is not None
|
||||
assert state.state == str(PLCNET.data_rates[0].tx_rate)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_update_last_update_auth_failed(
|
||||
hass: HomeAssistant, mock_device: MockDevice
|
||||
@@ -222,5 +210,3 @@ async def test_update_last_update_auth_failed(
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == entry.entry_id
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -35,17 +35,23 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_switch_setup(hass: HomeAssistant) -> None:
|
||||
async def test_switch_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the switch component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wi_fi") is not None
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_enable_guest_wi_fi"
|
||||
).disabled
|
||||
assert not entity_registry.async_get(
|
||||
f"{PLATFORM}.{device_name}_enable_leds"
|
||||
).disabled
|
||||
|
||||
|
||||
async def test_update_guest_wifi_status_auth_failed(
|
||||
@@ -70,8 +76,6 @@ async def test_update_guest_wifi_status_auth_failed(
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == entry.entry_id
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_update_enable_guest_wifi(
|
||||
hass: HomeAssistant,
|
||||
@@ -153,8 +157,6 @@ async def test_update_enable_guest_wifi(
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_update_enable_leds(
|
||||
hass: HomeAssistant,
|
||||
@@ -230,8 +232,6 @@ async def test_update_enable_leds(
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "get_method", "update_interval"),
|
||||
@@ -325,5 +325,3 @@ async def test_auth_failed(
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == entry.entry_id
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.devolo_home_network.const import (
|
||||
FIRMWARE_UPDATE_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.update import DOMAIN as PLATFORM, SERVICE_INSTALL
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -25,16 +25,18 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_update_setup(hass: HomeAssistant) -> None:
|
||||
async def test_update_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test default setup of the update component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.get(f"{PLATFORM}.{device_name}_firmware") is not None
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert not entity_registry.async_get(f"{PLATFORM}.{device_name}_firmware").disabled
|
||||
|
||||
|
||||
async def test_update_firmware(
|
||||
@@ -85,8 +87,6 @@ async def test_update_firmware(
|
||||
assert device_info is not None
|
||||
assert device_info.sw_version == mock_device.firmware_version
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_device_failure_check(
|
||||
hass: HomeAssistant,
|
||||
@@ -137,8 +137,6 @@ async def test_device_failure_update(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None:
|
||||
"""Test updating unauthorized triggers the reauth flow."""
|
||||
@@ -168,5 +166,3 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == entry.entry_id
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
@@ -79,12 +79,46 @@ async def test_get_user_data(
|
||||
assert res["result"]["value"]["test-complex"][0]["foo"] == "bar"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("subscriptions", "events"),
|
||||
[
|
||||
([], []),
|
||||
([(1, {}, {})], [(1, {"test-key": "test-value"})]),
|
||||
([(1, {"key": "test-key"}, None)], [(1, "test-value")]),
|
||||
([(1, {"key": "other-key"}, None)], []),
|
||||
],
|
||||
)
|
||||
async def test_set_user_data_empty(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
subscriptions: list[tuple[int, dict[str, str], Any]],
|
||||
events: list[tuple[int, Any]],
|
||||
) -> None:
|
||||
"""Test set_user_data command."""
|
||||
"""Test set_user_data command.
|
||||
|
||||
Also test subscribing.
|
||||
"""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
for msg_id, key, event_data in subscriptions:
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msg_id,
|
||||
"type": "frontend/subscribe_user_data",
|
||||
}
|
||||
| key
|
||||
)
|
||||
|
||||
event = await client.receive_json()
|
||||
assert event == {
|
||||
"id": msg_id,
|
||||
"type": "event",
|
||||
"event": {"value": event_data},
|
||||
}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
# test creating
|
||||
|
||||
await client.send_json(
|
||||
@@ -104,6 +138,10 @@ async def test_set_user_data_empty(
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
@@ -116,11 +154,63 @@ async def test_set_user_data_empty(
|
||||
assert res["result"]["value"] == "test-value"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("subscriptions", "events"),
|
||||
[
|
||||
(
|
||||
[],
|
||||
[[], []],
|
||||
),
|
||||
(
|
||||
[(1, {}, {"test-key": "test-value", "test-complex": "string"})],
|
||||
[
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"test-complex": "string",
|
||||
"test-key": "test-value",
|
||||
"test-non-existent-key": "test-value-new",
|
||||
},
|
||||
)
|
||||
],
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"test-complex": [{"foo": "bar"}],
|
||||
"test-key": "test-value",
|
||||
"test-non-existent-key": "test-value-new",
|
||||
},
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-key"}, "test-value")],
|
||||
[[], []],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-non-existent-key"}, None)],
|
||||
[[(1, "test-value-new")], []],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-complex"}, "string")],
|
||||
[[], [(1, [{"foo": "bar"}])]],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "other-key"}, None)],
|
||||
[[], []],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_set_user_data(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_admin_user: MockUser,
|
||||
subscriptions: list[tuple[int, dict[str, str], Any]],
|
||||
events: list[list[tuple[int, Any]]],
|
||||
) -> None:
|
||||
"""Test set_user_data command with initial data."""
|
||||
storage_key = f"{DOMAIN}.user_data_{hass_admin_user.id}"
|
||||
@@ -131,6 +221,25 @@ async def test_set_user_data(
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
for msg_id, key, event_data in subscriptions:
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msg_id,
|
||||
"type": "frontend/subscribe_user_data",
|
||||
}
|
||||
| key
|
||||
)
|
||||
|
||||
event = await client.receive_json()
|
||||
assert event == {
|
||||
"id": msg_id,
|
||||
"type": "event",
|
||||
"event": {"value": event_data},
|
||||
}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
# test creating
|
||||
|
||||
await client.send_json(
|
||||
@@ -142,6 +251,10 @@ async def test_set_user_data(
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events[0]:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
@@ -164,6 +277,10 @@ async def test_set_user_data(
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events[1]:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"connected": true,
|
||||
"type": "Hob",
|
||||
"enumber": "HCS000000/05",
|
||||
"haId": "BOSCH-HCS000000-D00000000005"
|
||||
"haId": "BOSCH-HCS000000-68A40E000000"
|
||||
},
|
||||
{
|
||||
"name": "CookProcessor",
|
||||
@@ -106,7 +106,7 @@
|
||||
"connected": true,
|
||||
"type": "CookProcessor",
|
||||
"enumber": "HCS000000/06",
|
||||
"haId": "BOSCH-HCS000000-D00000000006"
|
||||
"haId": "123456789012345678"
|
||||
},
|
||||
{
|
||||
"name": "DNE",
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
# serializer version: 1
|
||||
# name: test_async_get_config_entry_diagnostics
|
||||
dict({
|
||||
'123456789012345678': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/06',
|
||||
'ha_id': '123456789012345678',
|
||||
'name': 'CookProcessor',
|
||||
'programs': list([
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'CookProcessor',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'BOSCH-000000000-000000000000': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
@@ -21,6 +41,26 @@
|
||||
'type': 'DNE',
|
||||
'vib': 'HCS000000',
|
||||
}),
|
||||
'BOSCH-HCS000000-68A40E000000': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/05',
|
||||
'ha_id': 'BOSCH-HCS000000-68A40E000000',
|
||||
'name': 'Hob',
|
||||
'programs': list([
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Hob',
|
||||
'vib': 'HCS000005',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000001': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
@@ -114,46 +154,6 @@
|
||||
'type': 'Hood',
|
||||
'vib': 'HCS000004',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000005': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/05',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000005',
|
||||
'name': 'Hob',
|
||||
'programs': list([
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Hob',
|
||||
'vib': 'HCS000005',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000006': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/06',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000006',
|
||||
'name': 'CookProcessor',
|
||||
'programs': list([
|
||||
]),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'CookProcessor',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'BOSCH-HCS01OVN1-43E0065FE245': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Test the Home Connect config flow."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
from aiohomeconnect.model import HomeAppliance
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
@@ -11,7 +13,7 @@ from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN
|
||||
@@ -337,17 +339,17 @@ async def test_zeroconf_flow_already_setup(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize("dchp_discovery", DHCP_DISCOVERY)
|
||||
@pytest.mark.parametrize("dhcp_discovery", DHCP_DISCOVERY)
|
||||
async def test_dhcp_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
dchp_discovery: DhcpServiceInfo,
|
||||
dhcp_discovery: DhcpServiceInfo,
|
||||
) -> None:
|
||||
"""Test DHCP discovery."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dchp_discovery
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
@@ -391,8 +393,6 @@ async def test_dhcp_flow(
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_dhcp_flow_already_setup(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test DHCP discovery with already setup device."""
|
||||
@@ -403,3 +403,56 @@ async def test_dhcp_flow_already_setup(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
("dhcp_discovery", "appliance"),
|
||||
[
|
||||
(
|
||||
DhcpServiceInfo(
|
||||
ip="1.1.1.1",
|
||||
hostname="bosch-cookprocessor-123456789012345678",
|
||||
macaddress="c8:d7:78:00:00:00",
|
||||
),
|
||||
"CookProcessor",
|
||||
),
|
||||
(
|
||||
DhcpServiceInfo(
|
||||
ip="1.1.1.1",
|
||||
hostname="BOSCH-HCS000000-68A40E000000",
|
||||
macaddress="68:a4:0e:00:00:00",
|
||||
),
|
||||
"Hob",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_dhcp_flow_complete_device_information(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
dhcp_discovery: DhcpServiceInfo,
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test DHCP discovery with complete device information."""
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
assert device.connections == set()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
assert device.connections == {
|
||||
(dr.CONNECTION_NETWORK_MAC, dhcp_discovery.macaddress)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
MowerStates.IN_OPERATION,
|
||||
LawnMowerActivity.MOWING,
|
||||
),
|
||||
(
|
||||
MowerActivities.PARKED_IN_CS,
|
||||
MowerStates.IN_OPERATION,
|
||||
LawnMowerActivity.DOCKED,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lawn_mower_states(
|
||||
|
||||
@@ -94,8 +94,8 @@ async def test_step_user_existing_host(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_data
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {CONF_BASE: "already_configured"}
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -148,15 +148,16 @@ def upnp_notify_server_fixture(upnp_factory: Mock) -> Generator[Mock]:
|
||||
yield notify_server
|
||||
|
||||
|
||||
@pytest.fixture(name="remote")
|
||||
def remote_fixture() -> Generator[Mock]:
|
||||
@pytest.fixture(name="remote_legacy")
|
||||
def remote_legacy_fixture() -> Generator[Mock]:
|
||||
"""Patch the samsungctl Remote."""
|
||||
with patch("homeassistant.components.samsungtv.bridge.Remote") as remote_class:
|
||||
remote = Mock(Remote)
|
||||
remote.__enter__ = Mock()
|
||||
remote.__exit__ = Mock()
|
||||
remote_class.return_value = remote
|
||||
yield remote
|
||||
remote_legacy = Mock(Remote)
|
||||
remote_legacy.__enter__ = Mock()
|
||||
remote_legacy.__exit__ = Mock()
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.bridge.Remote", return_value=remote_legacy
|
||||
):
|
||||
yield remote_legacy
|
||||
|
||||
|
||||
@pytest.fixture(name="rest_api")
|
||||
@@ -208,8 +209,8 @@ def rest_api_failure_fixture() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="remoteencws_failing")
|
||||
def remoteencws_failing_fixture() -> Generator[None]:
|
||||
@pytest.fixture(name="remote_encrypted_websocket_failing")
|
||||
def remote_encrypted_websocket_failing_fixture() -> Generator[None]:
|
||||
"""Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote."""
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening",
|
||||
@@ -218,71 +219,77 @@ def remoteencws_failing_fixture() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="remotews")
|
||||
def remotews_fixture() -> Generator[Mock]:
|
||||
@pytest.fixture(name="remote_websocket")
|
||||
def remote_websocket_fixture() -> Generator[Mock]:
|
||||
"""Patch the samsungtvws SamsungTVWS."""
|
||||
remotews = Mock(SamsungTVWSAsyncRemote)
|
||||
remotews.__aenter__ = AsyncMock(return_value=remotews)
|
||||
remotews.__aexit__ = AsyncMock()
|
||||
remotews.token = "FAKE_TOKEN"
|
||||
remotews.app_list_data = None
|
||||
remote_websocket = Mock(SamsungTVWSAsyncRemote)
|
||||
remote_websocket.__aenter__ = AsyncMock(return_value=remote_websocket)
|
||||
remote_websocket.__aexit__ = AsyncMock()
|
||||
remote_websocket.token = "FAKE_TOKEN"
|
||||
remote_websocket.app_list_data = None
|
||||
|
||||
async def _start_listening(
|
||||
ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None,
|
||||
):
|
||||
remotews.ws_event_callback = ws_event_callback
|
||||
remote_websocket.ws_event_callback = ws_event_callback
|
||||
|
||||
async def _send_commands(commands: list[SamsungTVCommand]):
|
||||
if (
|
||||
len(commands) == 1
|
||||
and isinstance(commands[0], ChannelEmitCommand)
|
||||
and commands[0].params["event"] == "ed.installedApp.get"
|
||||
and remotews.app_list_data is not None
|
||||
and remote_websocket.app_list_data is not None
|
||||
):
|
||||
remotews.raise_mock_ws_event_callback(
|
||||
remote_websocket.raise_mock_ws_event_callback(
|
||||
ED_INSTALLED_APP_EVENT,
|
||||
remotews.app_list_data,
|
||||
remote_websocket.app_list_data,
|
||||
)
|
||||
|
||||
def _mock_ws_event_callback(event: str, response: Any):
|
||||
if remotews.ws_event_callback:
|
||||
remotews.ws_event_callback(event, response)
|
||||
if remote_websocket.ws_event_callback:
|
||||
remote_websocket.ws_event_callback(event, response)
|
||||
|
||||
remotews.start_listening.side_effect = _start_listening
|
||||
remotews.send_commands.side_effect = _send_commands
|
||||
remotews.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback)
|
||||
remote_websocket.start_listening.side_effect = _start_listening
|
||||
remote_websocket.send_commands.side_effect = _send_commands
|
||||
remote_websocket.raise_mock_ws_event_callback = Mock(
|
||||
side_effect=_mock_ws_event_callback
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote",
|
||||
) as remotews_class:
|
||||
remotews_class.return_value = remotews
|
||||
yield remotews
|
||||
return_value=remote_websocket,
|
||||
):
|
||||
yield remote_websocket
|
||||
|
||||
|
||||
@pytest.fixture(name="remoteencws")
|
||||
def remoteencws_fixture() -> Generator[Mock]:
|
||||
@pytest.fixture(name="remote_encrypted_websocket")
|
||||
def remote_encrypted_websocket_fixture() -> Generator[Mock]:
|
||||
"""Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote."""
|
||||
remoteencws = Mock(SamsungTVEncryptedWSAsyncRemote)
|
||||
remoteencws.__aenter__ = AsyncMock(return_value=remoteencws)
|
||||
remoteencws.__aexit__ = AsyncMock()
|
||||
remote_encrypted_websocket = Mock(SamsungTVEncryptedWSAsyncRemote)
|
||||
remote_encrypted_websocket.__aenter__ = AsyncMock(
|
||||
return_value=remote_encrypted_websocket
|
||||
)
|
||||
remote_encrypted_websocket.__aexit__ = AsyncMock()
|
||||
|
||||
def _start_listening(
|
||||
ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None,
|
||||
):
|
||||
remoteencws.ws_event_callback = ws_event_callback
|
||||
remote_encrypted_websocket.ws_event_callback = ws_event_callback
|
||||
|
||||
def _mock_ws_event_callback(event: str, response: Any):
|
||||
if remoteencws.ws_event_callback:
|
||||
remoteencws.ws_event_callback(event, response)
|
||||
if remote_encrypted_websocket.ws_event_callback:
|
||||
remote_encrypted_websocket.ws_event_callback(event, response)
|
||||
|
||||
remoteencws.start_listening.side_effect = _start_listening
|
||||
remoteencws.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback)
|
||||
remote_encrypted_websocket.start_listening.side_effect = _start_listening
|
||||
remote_encrypted_websocket.raise_mock_ws_event_callback = Mock(
|
||||
side_effect=_mock_ws_event_callback
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote",
|
||||
) as remotews_class:
|
||||
remotews_class.return_value = remoteencws
|
||||
yield remoteencws
|
||||
remotews_class.return_value = remote_encrypted_websocket
|
||||
yield remote_encrypted_websocket
|
||||
|
||||
|
||||
@pytest.fixture(name="mac_address", autouse=True)
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
from homeassistant.components.samsungtv.const import (
|
||||
CONF_SESSION_ID,
|
||||
DOMAIN,
|
||||
ENCRYPTED_WEBSOCKET_PORT,
|
||||
LEGACY_PORT,
|
||||
METHOD_ENCRYPTED_WEBSOCKET,
|
||||
METHOD_LEGACY,
|
||||
METHOD_WEBSOCKET,
|
||||
WEBSOCKET_SSL_PORT,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_METHOD,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_TOKEN,
|
||||
)
|
||||
@@ -19,37 +22,25 @@ from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
|
||||
MOCK_CONFIG = {
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_NAME: "fake",
|
||||
CONF_PORT: 55000,
|
||||
ENTRYDATA_LEGACY = {
|
||||
CONF_HOST: "10.10.12.34",
|
||||
CONF_PORT: LEGACY_PORT,
|
||||
CONF_METHOD: METHOD_LEGACY,
|
||||
}
|
||||
MOCK_CONFIG_ENCRYPTED_WS = {
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_NAME: "fake",
|
||||
CONF_PORT: 8000,
|
||||
}
|
||||
MOCK_ENTRYDATA_ENCRYPTED_WS = {
|
||||
**MOCK_CONFIG_ENCRYPTED_WS,
|
||||
CONF_METHOD: "encrypted",
|
||||
ENTRYDATA_ENCRYPTED_WEBSOCKET = {
|
||||
CONF_HOST: "10.10.12.34",
|
||||
CONF_PORT: ENCRYPTED_WEBSOCKET_PORT,
|
||||
CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET,
|
||||
CONF_MAC: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_TOKEN: "037739871315caef138547b03e348b72",
|
||||
CONF_SESSION_ID: "2",
|
||||
}
|
||||
MOCK_ENTRYDATA_WS = {
|
||||
ENTRYDATA_WEBSOCKET = {
|
||||
CONF_HOST: "10.10.12.34",
|
||||
CONF_METHOD: METHOD_WEBSOCKET,
|
||||
CONF_PORT: 8002,
|
||||
CONF_MODEL: "any",
|
||||
CONF_NAME: "any",
|
||||
}
|
||||
MOCK_ENTRY_WS_WITH_MAC = {
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_METHOD: "websocket",
|
||||
CONF_PORT: WEBSOCKET_SSL_PORT,
|
||||
CONF_MAC: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_NAME: "fake",
|
||||
CONF_PORT: 8002,
|
||||
CONF_MODEL: "UE43LS003",
|
||||
CONF_TOKEN: "123456789",
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
}),
|
||||
'entry': dict({
|
||||
'data': dict({
|
||||
'host': 'fake_host',
|
||||
'host': '10.10.12.34',
|
||||
'mac': 'aa:bb:cc:dd:ee:ff',
|
||||
'method': 'websocket',
|
||||
'model': '82GXARRS',
|
||||
'name': 'fake',
|
||||
'model': 'UE43LS003',
|
||||
'port': 8002,
|
||||
'token': '**REDACTED**',
|
||||
}),
|
||||
@@ -45,10 +44,9 @@
|
||||
'device_info': None,
|
||||
'entry': dict({
|
||||
'data': dict({
|
||||
'host': 'fake_host',
|
||||
'host': '10.10.12.34',
|
||||
'mac': 'aa:bb:cc:dd:ee:ff',
|
||||
'method': 'encrypted',
|
||||
'name': 'fake',
|
||||
'port': 8000,
|
||||
'session_id': '**REDACTED**',
|
||||
'token': '**REDACTED**',
|
||||
@@ -104,11 +102,10 @@
|
||||
}),
|
||||
'entry': dict({
|
||||
'data': dict({
|
||||
'host': 'fake_host',
|
||||
'host': '10.10.12.34',
|
||||
'mac': 'aa:bb:cc:dd:ee:ff',
|
||||
'method': 'encrypted',
|
||||
'model': 'UE48JU6400',
|
||||
'name': 'fake',
|
||||
'port': 8000,
|
||||
'session_id': '**REDACTED**',
|
||||
'token': '**REDACTED**',
|
||||
|
||||
@@ -1,90 +1,4 @@
|
||||
# serializer version: 1
|
||||
# name: test_cleanup_mac
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
tuple(
|
||||
'mac',
|
||||
'none',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'samsungtv',
|
||||
'any',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': '82GXARRS',
|
||||
'model_id': None,
|
||||
'name': 'fake',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_cleanup_mac.1
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'mac',
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
),
|
||||
tuple(
|
||||
'mac',
|
||||
'none',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'samsungtv',
|
||||
'any',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': '82GXARRS',
|
||||
'model_id': '82GXARRS',
|
||||
'name': 'fake',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_setup_updates_from_ssdp
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.components.samsungtv.const import (
|
||||
DEFAULT_MANUFACTURER,
|
||||
DOMAIN,
|
||||
LEGACY_PORT,
|
||||
METHOD_LEGACY,
|
||||
RESULT_AUTH_MISSING,
|
||||
RESULT_CANNOT_CONNECT,
|
||||
RESULT_NOT_SUPPORTED,
|
||||
@@ -54,8 +55,9 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import (
|
||||
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
||||
MOCK_ENTRYDATA_WS,
|
||||
ENTRYDATA_ENCRYPTED_WEBSOCKET,
|
||||
ENTRYDATA_LEGACY,
|
||||
ENTRYDATA_WEBSOCKET,
|
||||
MOCK_SSDP_DATA,
|
||||
MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||
MOCK_SSDP_DATA_RENDERING_CONTROL_ST,
|
||||
@@ -71,7 +73,6 @@ MOCK_USER_DATA = {CONF_HOST: "fake_host"}
|
||||
MOCK_DHCP_DATA = DhcpServiceInfo(
|
||||
ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname"
|
||||
)
|
||||
EXISTING_IP = "192.168.40.221"
|
||||
MOCK_ZEROCONF_DATA = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
@@ -86,16 +87,6 @@ MOCK_ZEROCONF_DATA = ZeroconfServiceInfo(
|
||||
},
|
||||
type="mock_type",
|
||||
)
|
||||
MOCK_OLD_ENTRY = {
|
||||
CONF_HOST: "10.10.12.34",
|
||||
CONF_METHOD: "legacy",
|
||||
CONF_PORT: None,
|
||||
}
|
||||
MOCK_LEGACY_ENTRY = {
|
||||
CONF_HOST: EXISTING_IP,
|
||||
CONF_METHOD: "legacy",
|
||||
CONF_PORT: None,
|
||||
}
|
||||
MOCK_DEVICE_INFO = {
|
||||
"device": {
|
||||
"type": "Samsung SmartTV",
|
||||
@@ -109,7 +100,7 @@ AUTODETECT_LEGACY = {
|
||||
"name": "HomeAssistant",
|
||||
"description": "HomeAssistant",
|
||||
"id": "ha.component.samsung",
|
||||
"method": "legacy",
|
||||
"method": METHOD_LEGACY,
|
||||
"port": LEGACY_PORT,
|
||||
"host": "fake_host",
|
||||
"timeout": TIMEOUT_REQUEST,
|
||||
@@ -144,7 +135,7 @@ DEVICEINFO_WEBSOCKET_NO_SSL = {
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_user_legacy(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user."""
|
||||
# show form
|
||||
@@ -162,7 +153,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "fake_host"
|
||||
assert result["data"][CONF_HOST] == "fake_host"
|
||||
assert result["data"][CONF_METHOD] == "legacy"
|
||||
assert result["data"][CONF_METHOD] == METHOD_LEGACY
|
||||
assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER
|
||||
assert result["data"][CONF_MODEL] is None
|
||||
assert result["result"].unique_id is None
|
||||
@@ -196,13 +187,15 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None:
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "fake_host"
|
||||
assert result3["data"][CONF_HOST] == "fake_host"
|
||||
assert result3["data"][CONF_METHOD] == "legacy"
|
||||
assert result3["data"][CONF_METHOD] == METHOD_LEGACY
|
||||
assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER
|
||||
assert result3["data"][CONF_MODEL] is None
|
||||
assert result3["result"].unique_id is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_user_websocket(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user."""
|
||||
with patch(
|
||||
@@ -229,7 +222,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api_non_ssl_only")
|
||||
async def test_user_encrypted_websocket(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -322,7 +315,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing")
|
||||
async def test_user_websocket_not_supported(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user for not supported device."""
|
||||
with (
|
||||
@@ -343,7 +336,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing")
|
||||
async def test_user_websocket_access_denied(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -367,7 +360,7 @@ async def test_user_websocket_access_denied(
|
||||
assert "Please check the Device Connection Manager on your TV" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing")
|
||||
async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user for not supported device."""
|
||||
with (
|
||||
@@ -447,7 +440,7 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_CANNOT_CONNECT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_ssdp(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery."""
|
||||
# confirm to add the entry
|
||||
@@ -469,7 +462,7 @@ async def test_ssdp(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery when the manufacturer data is missing."""
|
||||
ssdp_data = deepcopy(MOCK_SSDP_DATA)
|
||||
@@ -487,7 +480,7 @@ async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"data", [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST]
|
||||
)
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_ssdp_legacy_not_remote_control_receiver_udn(
|
||||
hass: HomeAssistant, data: SsdpServiceInfo
|
||||
) -> None:
|
||||
@@ -499,7 +492,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn(
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_ssdp_noprefix(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery when friendly name doesn't start with [TV]."""
|
||||
ssdp_data = deepcopy(MOCK_SSDP_DATA)
|
||||
@@ -527,7 +520,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api_failing")
|
||||
async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery with authentication."""
|
||||
with patch(
|
||||
@@ -562,7 +555,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api_failing")
|
||||
async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery for not supported device."""
|
||||
with patch(
|
||||
@@ -577,7 +570,9 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -606,7 +601,9 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location(
|
||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_location(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -635,7 +632,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc
|
||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api_non_ssl_only")
|
||||
async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -720,8 +717,8 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None:
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote",
|
||||
) as remotews,
|
||||
patch.object(remotews, "open", side_effect=WebSocketException("Boom")),
|
||||
) as remote_websocket,
|
||||
patch.object(remote_websocket, "open", side_effect=WebSocketException("Boom")),
|
||||
):
|
||||
# device not supported
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -731,7 +728,7 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_CANNOT_CONNECT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote")
|
||||
@pytest.mark.usefixtures("remote_legacy")
|
||||
async def test_ssdp_wrong_manufacturer(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery."""
|
||||
ssdp_data = deepcopy(MOCK_SSDP_DATA)
|
||||
@@ -746,7 +743,7 @@ async def test_ssdp_wrong_manufacturer(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket_failing")
|
||||
async def test_ssdp_not_successful(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery but no device found."""
|
||||
with (
|
||||
@@ -778,7 +775,7 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_CANNOT_CONNECT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket_failing")
|
||||
async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery but no device found."""
|
||||
with (
|
||||
@@ -810,7 +807,7 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_CANNOT_CONNECT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "remote_encrypted_websocket_failing")
|
||||
async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery twice."""
|
||||
with patch(
|
||||
@@ -832,7 +829,7 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_ALREADY_IN_PROGRESS
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing")
|
||||
async def test_ssdp_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from discovery when already configured."""
|
||||
with patch(
|
||||
@@ -860,7 +857,9 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None:
|
||||
assert entry.unique_id == "123"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api_non_ssl_only", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api_non_ssl_only", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_dhcp_wireless(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from dhcp."""
|
||||
# confirm to add the entry
|
||||
@@ -886,7 +885,9 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None:
|
||||
"""Test starting a flow from dhcp."""
|
||||
# Even though it is named "wifiMac", it matches the mac of the wired connection
|
||||
@@ -916,7 +917,9 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None:
|
||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api_non_ssl_only", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api_non_ssl_only", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("source1", "data1", "source2", "data2", "is_matching_result"),
|
||||
[
|
||||
@@ -988,7 +991,9 @@ async def test_dhcp_zeroconf_already_in_progress(
|
||||
assert return_values == [is_matching_result]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_zeroconf(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from zeroconf."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1013,7 +1018,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing")
|
||||
async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> None:
|
||||
"""Test starting a flow from zeroconf where the device is actually a soundbar."""
|
||||
rest_api.rest_device_info.return_value = {
|
||||
@@ -1036,7 +1041,12 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) ->
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "remotews", "remoteencws", "rest_api_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_legacy",
|
||||
"remote_websocket",
|
||||
"remote_encrypted_websocket",
|
||||
"rest_api_failing",
|
||||
)
|
||||
async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from zeroconf where device_info returns None."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1049,7 +1059,9 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow from zeroconf and dhcp."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1071,7 +1083,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None:
|
||||
assert result2["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket_failing")
|
||||
async def test_autodetect_websocket(hass: HomeAssistant) -> None:
|
||||
"""Test for send key with autodetection of protocol."""
|
||||
with (
|
||||
@@ -1081,7 +1093,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None:
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote"
|
||||
) as remotews,
|
||||
) as remote_websocket,
|
||||
patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest",
|
||||
) as rest_api_class,
|
||||
@@ -1104,7 +1116,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None:
|
||||
}
|
||||
)
|
||||
remote.token = "123456789"
|
||||
remotews.return_value = remote
|
||||
remote_websocket.return_value = remote
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
|
||||
@@ -1112,7 +1124,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_METHOD] == "websocket"
|
||||
assert result["data"][CONF_TOKEN] == "123456789"
|
||||
remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL)
|
||||
remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL)
|
||||
rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1121,7 +1133,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None:
|
||||
assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket_failing")
|
||||
async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None:
|
||||
"""Test for send key with autodetection of protocol."""
|
||||
mac_address.return_value = "gg:ee:tt:mm:aa:cc"
|
||||
@@ -1132,7 +1144,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None:
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote"
|
||||
) as remotews,
|
||||
) as remote_websocket,
|
||||
patch(
|
||||
"homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest",
|
||||
) as rest_api_class,
|
||||
@@ -1154,7 +1166,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None:
|
||||
)
|
||||
|
||||
remote.token = "123456789"
|
||||
remotews.return_value = remote
|
||||
remote_websocket.return_value = remote
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
|
||||
@@ -1163,7 +1175,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None:
|
||||
assert result["data"][CONF_METHOD] == "websocket"
|
||||
assert result["data"][CONF_TOKEN] == "123456789"
|
||||
assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc"
|
||||
remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL)
|
||||
remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL)
|
||||
rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1217,14 +1229,14 @@ async def test_autodetect_not_supported(hass: HomeAssistant) -> None:
|
||||
assert remote.call_args_list == [call(AUTODETECT_LEGACY)]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_autodetect_legacy(hass: HomeAssistant) -> None:
|
||||
"""Test for send key with autodetection of protocol."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_METHOD] == "legacy"
|
||||
assert result["data"][CONF_METHOD] == METHOD_LEGACY
|
||||
assert result["data"][CONF_MAC] is None
|
||||
assert result["data"][CONF_PORT] == LEGACY_PORT
|
||||
|
||||
@@ -1253,10 +1265,12 @@ async def test_autodetect_none(hass: HomeAssistant) -> None:
|
||||
assert rest_device_info.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_old_entry(hass: HomeAssistant) -> None:
|
||||
"""Test update of old entry sets unique id."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
config_entries_domain = hass.config_entries.async_entries(DOMAIN)
|
||||
@@ -1282,12 +1296,14 @@ async def test_update_old_entry(hass: HomeAssistant) -> None:
|
||||
assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_added_from_dhcp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing mac and unique id added."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY, unique_id=None)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1304,12 +1320,14 @@ async def test_update_missing_mac_unique_id_added_from_dhcp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test incorrectly formatted mac is updated and unique id added."""
|
||||
entry_data = MOCK_OLD_ENTRY.copy()
|
||||
entry_data = ENTRYDATA_LEGACY.copy()
|
||||
entry_data[CONF_MAC] = "aabbccddeeff"
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id=None)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1328,13 +1346,17 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_added_from_zeroconf(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing mac and unique id added."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={**MOCK_OLD_ENTRY, "host": "127.0.0.1"}, unique_id=None
|
||||
domain=DOMAIN,
|
||||
data={**ENTRYDATA_LEGACY, "host": "127.0.0.1"},
|
||||
unique_id=None,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@@ -1352,14 +1374,14 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "rest_api_failing")
|
||||
@pytest.mark.usefixtures("remote_legacy", "rest_api_failing")
|
||||
async def test_update_missing_model_added_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing model added via ssdp on legacy models."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_OLD_ENTRY,
|
||||
data=ENTRYDATA_LEGACY,
|
||||
unique_id=None,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1377,12 +1399,14 @@ async def test_update_missing_model_added_from_ssdp(
|
||||
assert entry.data[CONF_MODEL] == "UE55H6400"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing mac, ssdp_location, and unique id added via ssdp."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY, unique_id=None)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1402,7 +1426,10 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"remote", "remotews", "remoteencws_failing", "rest_api_failing"
|
||||
"remote_legacy",
|
||||
"remote_websocket",
|
||||
"remote_encrypted_websocket_failing",
|
||||
"rest_api_failing",
|
||||
)
|
||||
async def test_update_zeroconf_discovery_preserved_unique_id(
|
||||
hass: HomeAssistant,
|
||||
@@ -1410,7 +1437,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id(
|
||||
"""Test zeroconf discovery preserves unique id."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:zz:ee:rr:oo"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:zz:ee:rr:oo"},
|
||||
unique_id="original",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1426,7 +1453,9 @@ async def test_update_zeroconf_discovery_preserved_unique_id(
|
||||
assert entry.unique_id == "original"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
@@ -1434,7 +1463,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
**MOCK_OLD_ENTRY,
|
||||
**ENTRYDATA_LEGACY,
|
||||
CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test",
|
||||
},
|
||||
unique_id=None,
|
||||
@@ -1459,7 +1488,9 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
@@ -1467,7 +1498,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
**MOCK_OLD_ENTRY,
|
||||
**ENTRYDATA_LEGACY,
|
||||
CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test",
|
||||
},
|
||||
unique_id=None,
|
||||
@@ -1493,7 +1524,9 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
@@ -1501,7 +1534,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
**MOCK_OLD_ENTRY,
|
||||
**ENTRYDATA_LEGACY,
|
||||
CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test",
|
||||
CONF_SSDP_MAIN_TV_AGENT_LOCATION: "https://1.2.3.4:555/test",
|
||||
},
|
||||
@@ -1531,14 +1564,16 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_ssdp_location_rendering_st_updated_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test with outdated ssdp_location with the correct st added via ssdp."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
unique_id="be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1562,14 +1597,16 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test with outdated ssdp_location with the correct st added via ssdp."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
unique_id="be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1592,14 +1629,14 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing mac and unique id added."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, "host": "127.0.0.1"},
|
||||
data={**ENTRYDATA_LEGACY, "host": "127.0.0.1"},
|
||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1618,14 +1655,14 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf(
|
||||
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote")
|
||||
@pytest.mark.usefixtures("remote_legacy")
|
||||
async def test_update_legacy_missing_mac_from_dhcp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing mac added."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_LEGACY_ENTRY,
|
||||
data=ENTRYDATA_LEGACY,
|
||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1634,7 +1671,7 @@ async def test_update_legacy_missing_mac_from_dhcp(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=DhcpServiceInfo(
|
||||
ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname"
|
||||
ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname"
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1646,7 +1683,7 @@ async def test_update_legacy_missing_mac_from_dhcp(
|
||||
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote")
|
||||
@pytest.mark.usefixtures("remote_legacy")
|
||||
async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(
|
||||
hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
@@ -1654,7 +1691,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(
|
||||
rest_api.rest_device_info.side_effect = HttpApiError
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_LEGACY_ENTRY,
|
||||
data=ENTRYDATA_LEGACY,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with (
|
||||
@@ -1671,7 +1708,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=DhcpServiceInfo(
|
||||
ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname"
|
||||
ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname"
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1683,14 +1720,16 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(
|
||||
assert entry.unique_id is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_ssdp_location_unique_id_added_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing ssdp_location, and unique id added via ssdp."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
unique_id=None,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1711,14 +1750,16 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test missing ssdp_location, and unique id added via ssdp with rendering control st."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
unique_id=None,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1742,10 +1783,10 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote")
|
||||
@pytest.mark.usefixtures("remote_legacy")
|
||||
async def test_form_reauth_legacy(hass: HomeAssistant) -> None:
|
||||
"""Test reauthenticate legacy."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY)
|
||||
entry.add_to_hass(hass)
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -1760,10 +1801,10 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None:
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_form_reauth_websocket(hass: HomeAssistant) -> None:
|
||||
"""Test reauthenticate websocket."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET)
|
||||
entry.add_to_hass(hass)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
@@ -1783,16 +1824,16 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.usefixtures("rest_api")
|
||||
async def test_form_reauth_websocket_cannot_connect(
|
||||
hass: HomeAssistant, remotews: Mock
|
||||
hass: HomeAssistant, remote_websocket: Mock
|
||||
) -> None:
|
||||
"""Test reauthenticate websocket when we cannot connect on the first attempt."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET)
|
||||
entry.add_to_hass(hass)
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch.object(remotews, "open", side_effect=ConnectionFailure):
|
||||
with patch.object(remote_websocket, "open", side_effect=ConnectionFailure):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
@@ -1814,7 +1855,7 @@ async def test_form_reauth_websocket_cannot_connect(
|
||||
|
||||
async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None:
|
||||
"""Test reauthenticate websocket when the device is not supported."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET)
|
||||
entry.add_to_hass(hass)
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -1834,10 +1875,10 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None:
|
||||
assert result2["reason"] == RESULT_NOT_SUPPORTED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_form_reauth_encrypted(hass: HomeAssistant) -> None:
|
||||
"""Test reauth flow for encrypted TVs."""
|
||||
encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS}
|
||||
encrypted_entry_data = deepcopy(ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
del encrypted_entry_data[CONF_TOKEN]
|
||||
del encrypted_entry_data[CONF_SESSION_ID]
|
||||
|
||||
@@ -1890,7 +1931,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None:
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
authenticator_mock.assert_called_once()
|
||||
assert authenticator_mock.call_args[0] == ("fake_host",)
|
||||
assert authenticator_mock.call_args[0] == ("10.10.12.34",)
|
||||
|
||||
authenticator_mock.return_value.start_pairing.assert_called_once()
|
||||
assert authenticator_mock.return_value.try_pin.call_count == 2
|
||||
@@ -1904,14 +1945,16 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None:
|
||||
assert entry.data[CONF_SESSION_ID] == "1"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test updating the wrong udn from ssdp via upnp udn match."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_OLD_ENTRY,
|
||||
data=ENTRYDATA_LEGACY,
|
||||
unique_id="068e7781-006e-1000-bbbf-84a4668d8423",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1930,14 +1973,16 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test updating the wrong udn from ssdp via mac match."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
unique_id=None,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -1956,14 +2001,16 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_update_incorrect_udn_matching_mac_from_dhcp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test that DHCP updates the wrong udn from ssdp via mac match."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
data={**ENTRYDATA_WEBSOCKET, CONF_MAC: "aa:bb:aa:aa:aa:aa"},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||
)
|
||||
@@ -1983,14 +2030,16 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp(
|
||||
assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "rest_api", "remote_encrypted_websocket_failing"
|
||||
)
|
||||
async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test that DHCP does not update the wrong udn from ssdp via host match."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ss:ss:dd:pp"},
|
||||
data={**ENTRYDATA_WEBSOCKET, CONF_MAC: "aa:bb:ss:ss:dd:pp"},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de",
|
||||
)
|
||||
@@ -2010,7 +2059,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp(
|
||||
assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing")
|
||||
async def test_ssdp_update_mac(hass: HomeAssistant) -> None:
|
||||
"""Ensure that MAC address is correctly updated from SSDP."""
|
||||
with patch(
|
||||
|
||||
@@ -16,17 +16,17 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import setup_samsungtv_entry
|
||||
from .const import MOCK_ENTRYDATA_ENCRYPTED_WS
|
||||
from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET
|
||||
|
||||
from tests.common import MockConfigEntry, async_get_device_automations
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_get_triggers(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test we get the expected triggers."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "be9554b9-c9fb-41f4-8920-22da015376a4")}
|
||||
@@ -46,14 +46,14 @@ async def test_get_triggers(
|
||||
assert turn_on_trigger in triggers
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_if_fires_on_turn_on_request(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test for turn_on and turn_off triggers firing."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
entity_id = "media_player.mock_title"
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
@@ -109,12 +109,12 @@ async def test_if_fires_on_turn_on_request(
|
||||
assert service_calls[2].data["id"] == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_failure_scenarios(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test failure scenarios."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
# Test wrong trigger platform type
|
||||
with pytest.raises(HomeAssistantError):
|
||||
|
||||
@@ -11,28 +11,28 @@ from homeassistant.components.samsungtv.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_samsungtv_entry
|
||||
from .const import MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS
|
||||
from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_WEBSOCKET
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS_WITH_MAC)
|
||||
config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_WEBSOCKET)
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
) == snapshot(exclude=props("created_at", "modified_at"))
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket")
|
||||
async def test_entry_diagnostics_encrypted(
|
||||
hass: HomeAssistant,
|
||||
rest_api: Mock,
|
||||
@@ -43,14 +43,14 @@ async def test_entry_diagnostics_encrypted(
|
||||
rest_api.rest_device_info.return_value = load_json_object_fixture(
|
||||
"device_info_UE48JU6400.json", DOMAIN
|
||||
)
|
||||
config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
) == snapshot(exclude=props("created_at", "modified_at"))
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket")
|
||||
async def test_entry_diagnostics_encrypte_offline(
|
||||
hass: HomeAssistant,
|
||||
rest_api: Mock,
|
||||
@@ -59,7 +59,7 @@ async def test_entry_diagnostics_encrypte_offline(
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
rest_api.rest_device_info.side_effect = HttpApiError
|
||||
config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
assert await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for the Samsung TV Integration."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -11,7 +12,6 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.components.samsungtv.const import (
|
||||
CONF_MANUFACTURER,
|
||||
CONF_SESSION_ID,
|
||||
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
|
||||
CONF_SSDP_RENDERING_CONTROL_LOCATION,
|
||||
@@ -36,13 +36,12 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_samsungtv_entry
|
||||
from .const import (
|
||||
MOCK_ENTRY_WS_WITH_MAC,
|
||||
MOCK_ENTRYDATA_ENCRYPTED_WS,
|
||||
MOCK_ENTRYDATA_WS,
|
||||
ENTRYDATA_ENCRYPTED_WEBSOCKET,
|
||||
ENTRYDATA_WEBSOCKET,
|
||||
MOCK_SSDP_DATA_MAIN_TV_AGENT_ST,
|
||||
MOCK_SSDP_DATA_RENDERING_CONTROL_ST,
|
||||
)
|
||||
@@ -57,7 +56,9 @@ MOCK_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "remote_encrypted_websocket_failing", "rest_api"
|
||||
)
|
||||
async def test_setup(hass: HomeAssistant) -> None:
|
||||
"""Test Samsung TV integration is setup."""
|
||||
await setup_samsungtv_entry(hass, MOCK_CONFIG)
|
||||
@@ -101,7 +102,9 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None:
|
||||
assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api")
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_websocket", "remote_encrypted_websocket_failing", "rest_api"
|
||||
)
|
||||
async def test_setup_without_port_device_online(hass: HomeAssistant) -> None:
|
||||
"""Test import from yaml when the device is online."""
|
||||
await setup_samsungtv_entry(hass, MOCK_CONFIG)
|
||||
@@ -111,7 +114,7 @@ async def test_setup_without_port_device_online(hass: HomeAssistant) -> None:
|
||||
assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing")
|
||||
@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing")
|
||||
async def test_setup_h_j_model(
|
||||
hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -126,13 +129,13 @@ async def test_setup_h_j_model(
|
||||
assert "H and J series use an encrypted protocol" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_setup_updates_from_ssdp(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test setting up the entry fetches data from ssdp cache."""
|
||||
entry = MockConfigEntry(
|
||||
domain="samsungtv", data=MOCK_ENTRYDATA_WS, entry_id="sample-entry-id"
|
||||
domain="samsungtv", data=ENTRYDATA_WEBSOCKET, entry_id="sample-entry-id"
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@@ -162,10 +165,10 @@ async def test_setup_updates_from_ssdp(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None:
|
||||
"""Test reauth flow is triggered for encrypted TVs."""
|
||||
encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS}
|
||||
encrypted_entry_data = {**ENTRYDATA_ENCRYPTED_WEBSOCKET}
|
||||
del encrypted_entry_data[CONF_TOKEN]
|
||||
del encrypted_entry_data[CONF_SESSION_ID]
|
||||
|
||||
@@ -179,12 +182,22 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None:
|
||||
assert len(flows_in_progress) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remote", "remotews", "rest_api_failing")
|
||||
async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> None:
|
||||
"""Test updating an imported legacy entry without a method."""
|
||||
await setup_samsungtv_entry(
|
||||
hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"}
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
"remote_legacy", "remote_encrypted_websocket_failing", "rest_api_failing"
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"entry_data",
|
||||
[
|
||||
{CONF_HOST: "1.2.3.4"}, # Missing port/method
|
||||
{CONF_HOST: "1.2.3.4", CONF_PORT: LEGACY_PORT}, # Missing method
|
||||
{CONF_HOST: "1.2.3.4", CONF_METHOD: METHOD_LEGACY}, # Missing port
|
||||
],
|
||||
)
|
||||
async def test_update_imported_legacy(
|
||||
hass: HomeAssistant, entry_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Test updating an imported legacy entry."""
|
||||
await setup_samsungtv_entry(hass, entry_data)
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
@@ -192,7 +205,7 @@ async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> Non
|
||||
assert entries[0].data[CONF_PORT] == LEGACY_PORT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None:
|
||||
"""Test incorrectly formatted mac is corrected."""
|
||||
with patch(
|
||||
@@ -220,54 +233,3 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None:
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.xfail
|
||||
async def test_cleanup_mac(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test for `none` mac cleanup #103512.
|
||||
|
||||
Reverted due to device registry collisions in #119249 / #119082
|
||||
"""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_ENTRY_WS_WITH_MAC,
|
||||
entry_id="123456",
|
||||
unique_id="be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||
version=2,
|
||||
minor_version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# Setup initial device registry, with incorrect MAC
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id="123456",
|
||||
connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, "none"),
|
||||
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff"),
|
||||
},
|
||||
identifiers={("samsungtv", "be9554b9-c9fb-41f4-8920-22da015376a4")},
|
||||
model="82GXARRS",
|
||||
name="fake",
|
||||
)
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
assert device_entries == snapshot
|
||||
assert device_entries[0].connections == {
|
||||
(dr.CONNECTION_NETWORK_MAC, "none"),
|
||||
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff"),
|
||||
}
|
||||
|
||||
# Run setup, and ensure the NONE mac is removed
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
assert device_entries == snapshot
|
||||
assert device_entries[0].connections == {
|
||||
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
|
||||
}
|
||||
|
||||
assert entry.version == 2
|
||||
assert entry.minor_version == 2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,39 +17,41 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_samsungtv_entry
|
||||
from .const import MOCK_CONFIG, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS
|
||||
from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_LEGACY, ENTRYDATA_WEBSOCKET
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ENTITY_ID = f"{REMOTE_DOMAIN}.mock_title"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_setup(hass: HomeAssistant) -> None:
|
||||
"""Test setup with basic config."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
assert hass.states.get(ENTITY_ID)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_unique_id(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test unique id."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
main = entity_registry.async_get(ENTITY_ID)
|
||||
assert main.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_main_services(
|
||||
hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant,
|
||||
remote_encrypted_websocket: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test for turn_off."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
remoteencws.send_commands.reset_mock()
|
||||
remote_encrypted_websocket.send_commands.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -59,8 +61,8 @@ async def test_main_services(
|
||||
)
|
||||
|
||||
# key called
|
||||
assert remoteencws.send_commands.call_count == 1
|
||||
commands = remoteencws.send_commands.call_args_list[0].args[0]
|
||||
assert remote_encrypted_websocket.send_commands.call_count == 1
|
||||
commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0]
|
||||
assert len(commands) == 2
|
||||
assert isinstance(command := commands[0], SamsungTVEncryptedCommand)
|
||||
assert command.body["param3"] == "KEY_POWEROFF"
|
||||
@@ -68,7 +70,7 @@ async def test_main_services(
|
||||
assert command.body["param3"] == "KEY_POWER"
|
||||
|
||||
# commands not sent : power off in progress
|
||||
remoteencws.send_commands.reset_mock()
|
||||
remote_encrypted_websocket.send_commands.reset_mock()
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
@@ -76,13 +78,15 @@ async def test_main_services(
|
||||
blocking=True,
|
||||
)
|
||||
assert "TV is powering off, not sending keys: ['dash']" in caplog.text
|
||||
remoteencws.send_commands.assert_not_called()
|
||||
remote_encrypted_websocket.send_commands.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> None:
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
async def test_send_command_service(
|
||||
hass: HomeAssistant, remote_encrypted_websocket: Mock
|
||||
) -> None:
|
||||
"""Test the send command."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
@@ -91,19 +95,19 @@ async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> N
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert remoteencws.send_commands.call_count == 1
|
||||
commands = remoteencws.send_commands.call_args_list[0].args[0]
|
||||
assert remote_encrypted_websocket.send_commands.call_count == 1
|
||||
commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0]
|
||||
assert len(commands) == 1
|
||||
assert isinstance(command := commands[0], SamsungTVEncryptedCommand)
|
||||
assert command.body["param3"] == "dash"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remotews", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_websocket", "rest_api")
|
||||
async def test_turn_on_wol(hass: HomeAssistant) -> None:
|
||||
"""Test turn on."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_ENTRY_WS_WITH_MAC,
|
||||
data=ENTRYDATA_WEBSOCKET,
|
||||
unique_id="be9554b9-c9fb-41f4-8920-22da015376a4",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
@@ -119,15 +123,15 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None:
|
||||
assert mock_send_magic_packet.called
|
||||
|
||||
|
||||
async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None:
|
||||
async def test_turn_on_without_turnon(hass: HomeAssistant, remote_legacy: Mock) -> None:
|
||||
"""Test turn on."""
|
||||
await setup_samsungtv_entry(hass, MOCK_CONFIG)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY)
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||
)
|
||||
# nothing called as not supported feature
|
||||
assert remote.control.call_count == 0
|
||||
assert remote_legacy.control.call_count == 0
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == "service_unsupported"
|
||||
assert exc_info.value.translation_placeholders == {
|
||||
|
||||
@@ -12,12 +12,12 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import setup_samsungtv_entry
|
||||
from .const import MOCK_ENTRYDATA_ENCRYPTED_WS
|
||||
from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET
|
||||
|
||||
from tests.common import MockEntity, MockEntityPlatform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
@pytest.mark.parametrize("entity_domain", ["media_player", "remote"])
|
||||
async def test_turn_on_trigger_device_id(
|
||||
hass: HomeAssistant,
|
||||
@@ -26,7 +26,7 @@ async def test_turn_on_trigger_device_id(
|
||||
entity_domain: str,
|
||||
) -> None:
|
||||
"""Test for turn_on triggers by device_id firing."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
entity_id = f"{entity_domain}.mock_title"
|
||||
|
||||
@@ -84,13 +84,13 @@ async def test_turn_on_trigger_device_id(
|
||||
mock_send_magic_packet.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
@pytest.mark.parametrize("entity_domain", ["media_player", "remote"])
|
||||
async def test_turn_on_trigger_entity_id(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall], entity_domain: str
|
||||
) -> None:
|
||||
"""Test for turn_on triggers by entity_id firing."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
|
||||
entity_id = f"{entity_domain}.mock_title"
|
||||
|
||||
@@ -126,13 +126,13 @@ async def test_turn_on_trigger_entity_id(
|
||||
assert service_calls[1].data["id"] == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
@pytest.mark.parametrize("entity_domain", ["media_player", "remote"])
|
||||
async def test_wrong_trigger_platform_type(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_domain: str
|
||||
) -> None:
|
||||
"""Test wrong trigger platform type."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
entity_id = f"{entity_domain}.fake"
|
||||
|
||||
await async_setup_component(
|
||||
@@ -163,13 +163,13 @@ async def test_wrong_trigger_platform_type(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("remoteencws", "rest_api")
|
||||
@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api")
|
||||
@pytest.mark.parametrize("entity_domain", ["media_player", "remote"])
|
||||
async def test_trigger_invalid_entity_id(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_domain: str
|
||||
) -> None:
|
||||
"""Test turn on trigger using invalid entity_id."""
|
||||
await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS)
|
||||
await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET)
|
||||
entity_id = f"{entity_domain}.fake"
|
||||
|
||||
platform = MockEntityPlatform(hass)
|
||||
|
||||
@@ -1078,3 +1078,21 @@ async def test_xmod_model_lookup(
|
||||
)
|
||||
assert device
|
||||
assert device.model == xmod_model
|
||||
|
||||
|
||||
async def test_device_entry_bt_address(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
) -> None:
|
||||
"""Check if BT address is added to device entry connections."""
|
||||
entry = await init_integration(hass, 2)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
|
||||
)
|
||||
|
||||
assert device
|
||||
assert len(device.connections) == 2
|
||||
assert (dr.CONNECTION_BLUETOOTH, "12:34:56:78:9A:BE") in device.connections
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -281,6 +281,7 @@ class MockResultStream(ResultStream):
|
||||
content_type=f"audio/mock-{extension}",
|
||||
engine="test-engine",
|
||||
use_file_cache=True,
|
||||
supports_streaming_input=True,
|
||||
language="en",
|
||||
options={},
|
||||
_manager=hass.data[DATA_TTS_MANAGER],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the TTS entity."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import tts
|
||||
@@ -142,3 +144,34 @@ async def test_tts_entity_subclass_properties(
|
||||
if record.exc_info is not None
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_streaming_supported() -> None:
|
||||
"""Test streaming support."""
|
||||
base_entity = tts.TextToSpeechEntity()
|
||||
assert base_entity.async_supports_streaming_input() is False
|
||||
|
||||
class StreamingEntity(tts.TextToSpeechEntity):
|
||||
async def async_stream_tts_audio(self) -> None:
|
||||
pass
|
||||
|
||||
streaming_entity = StreamingEntity()
|
||||
assert streaming_entity.async_supports_streaming_input() is True
|
||||
|
||||
class NonStreamingEntity(tts.TextToSpeechEntity):
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> tts.TtsAudioType:
|
||||
pass
|
||||
|
||||
non_streaming_entity = NonStreamingEntity()
|
||||
assert non_streaming_entity.async_supports_streaming_input() is False
|
||||
|
||||
class SyncNonStreamingEntity(tts.TextToSpeechEntity):
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> tts.TtsAudioType:
|
||||
pass
|
||||
|
||||
sync_non_streaming_entity = SyncNonStreamingEntity()
|
||||
assert sync_non_streaming_entity.async_supports_streaming_input() is False
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -1885,6 +1885,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No
|
||||
stream = tts.async_create_stream(hass, mock_tts_entity.entity_id)
|
||||
assert stream.language == mock_tts_entity.default_language
|
||||
assert stream.options == (mock_tts_entity.default_options or {})
|
||||
assert stream.supports_streaming_input is False
|
||||
assert tts.async_get_stream(hass, stream.token) is stream
|
||||
stream.async_set_message("beer")
|
||||
result_data = b"".join([chunk async for chunk in stream.async_stream_result()])
|
||||
@@ -1905,6 +1906,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No
|
||||
)
|
||||
|
||||
mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio
|
||||
mock_tts_entity.async_supports_streaming_input = Mock(return_value=True)
|
||||
|
||||
async def stream_message():
|
||||
"""Mock stream message."""
|
||||
@@ -1913,6 +1915,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No
|
||||
yield "o"
|
||||
|
||||
stream = tts.async_create_stream(hass, mock_tts_entity.entity_id)
|
||||
assert stream.supports_streaming_input is True
|
||||
stream.async_set_message_stream(stream_message())
|
||||
result_data = b"".join([chunk async for chunk in stream.async_stream_result()])
|
||||
assert result_data == b"hello"
|
||||
|
||||
@@ -2529,9 +2529,8 @@ async def test_validate_config_works(
|
||||
"state": "paulus",
|
||||
},
|
||||
(
|
||||
"Unexpected value for condition: 'non_existing'. Expected and, device,"
|
||||
" not, numeric_state, or, state, sun, template, time, trigger, zone "
|
||||
"@ data[0]"
|
||||
"Invalid condition \"non_existing\" specified {'condition': "
|
||||
"'non_existing', 'entity_id': 'hello.world', 'state': 'paulus'}"
|
||||
),
|
||||
),
|
||||
# Raises HomeAssistantError
|
||||
|
||||
@@ -843,7 +843,11 @@ async def integration_fixture(
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the zwave_js integration."""
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry = MockConfigEntry(
|
||||
domain="zwave_js",
|
||||
data={"url": "ws://test.org"},
|
||||
unique_id=str(client.driver.controller.home_id),
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.zwave_js.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
@@ -268,3 +269,118 @@ async def test_abort_confirm(
|
||||
assert data["type"] == "abort"
|
||||
assert data["reason"] == "cannot_connect"
|
||||
assert data["description_placeholders"] == {"device_name": device.name}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
async def test_migrate_unique_id(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the migrate unique id flow."""
|
||||
old_unique_id = "123456789"
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Z-Wave JS",
|
||||
data={
|
||||
"url": "ws://test.org",
|
||||
},
|
||||
unique_id=old_unique_id,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
ws_client = await hass_ws_client(hass)
|
||||
http_client = await hass_client()
|
||||
|
||||
# Assert the issue is present
|
||||
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
issue = msg["result"]["issues"][0]
|
||||
issue_id = issue["issue_id"]
|
||||
assert issue_id == f"migrate_unique_id.{config_entry.entry_id}"
|
||||
|
||||
data = await start_repair_fix_flow(http_client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "confirm"
|
||||
assert data["description_placeholders"] == {
|
||||
"config_entry_title": "Z-Wave JS",
|
||||
"controller_model": "ZW090",
|
||||
"new_unique_id": "3245146787",
|
||||
"old_unique_id": old_unique_id,
|
||||
}
|
||||
|
||||
# Apply fix
|
||||
data = await process_repair_fix_flow(http_client, flow_id)
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
assert config_entry.unique_id == "3245146787"
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
async def test_migrate_unique_id_missing_config_entry(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the migrate unique id flow with missing config entry."""
|
||||
old_unique_id = "123456789"
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Z-Wave JS",
|
||||
data={
|
||||
"url": "ws://test.org",
|
||||
},
|
||||
unique_id=old_unique_id,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
ws_client = await hass_ws_client(hass)
|
||||
http_client = await hass_client()
|
||||
|
||||
# Assert the issue is present
|
||||
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
issue = msg["result"]["issues"][0]
|
||||
issue_id = issue["issue_id"]
|
||||
assert issue_id == f"migrate_unique_id.{config_entry.entry_id}"
|
||||
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
|
||||
assert not hass.config_entries.async_get_entry(config_entry.entry_id)
|
||||
|
||||
data = await start_repair_fix_flow(http_client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["step_id"] == "confirm"
|
||||
assert data["description_placeholders"] == {
|
||||
"config_entry_title": "Z-Wave JS",
|
||||
"controller_model": "ZW090",
|
||||
"new_unique_id": "3245146787",
|
||||
"old_unique_id": old_unique_id,
|
||||
}
|
||||
|
||||
# Apply fix
|
||||
data = await process_repair_fix_flow(http_client, flow_id)
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 0
|
||||
|
||||
+30
-1226
File diff suppressed because it is too large
Load Diff
@@ -1460,13 +1460,14 @@ def test_key_value_schemas_with_default() -> None:
|
||||
[
|
||||
({"delay": "{{ invalid"}, "should be format 'HH:MM'"),
|
||||
({"wait_template": "{{ invalid"}, "invalid template"),
|
||||
({"condition": "invalid"}, "Unexpected value for condition: 'invalid'"),
|
||||
(
|
||||
{"condition": "not", "conditions": {"condition": "invalid"}},
|
||||
"Unexpected value for condition: 'invalid'",
|
||||
),
|
||||
# The validation error message could be improved to explain that this is not
|
||||
# a valid shorthand template
|
||||
(
|
||||
{"condition": 123},
|
||||
"Unexpected value for condition: '123'. Expected and, device, not, "
|
||||
"numeric_state, or, state, template, time, trigger, zone, a list of "
|
||||
"conditions or a valid template",
|
||||
),
|
||||
(
|
||||
{"condition": "not", "conditions": "not a dynamic template"},
|
||||
"Expected a dictionary",
|
||||
@@ -1496,7 +1497,7 @@ def test_key_value_schemas_with_default() -> None:
|
||||
)
|
||||
@pytest.mark.usefixtures("hass")
|
||||
def test_script(caplog: pytest.LogCaptureFixture, config: dict, error: str) -> None:
|
||||
"""Test script validation is user friendly."""
|
||||
"""Test script action validation is user friendly."""
|
||||
with pytest.raises(vol.Invalid, match=error):
|
||||
cv.script_action(config)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user