Compare commits

...

37 Commits

Author SHA1 Message Date
Erik c705736739 Improve condition schema validation 2025-05-13 08:22:16 +02:00
Maciej Bieniek b15c9ad130 Link Shelly device entry with Shelly BT scanner entry (#144626)
* Add BT address to DeviceInfo.connections

* Cleaning

* Use bluetooth_source property

* Add test

* Add connections property
2025-05-13 07:19:07 +02:00
Erik Montnemery 0128d85999 Move sun conditions to the sun integration (#144742) 2025-05-12 23:03:37 +01:00
David Rapan e69ca0cf80 Bump aiodhcpwatcher to 1.2.0 (#144769) 2025-05-12 17:00:17 -05:00
Åke Strandberg 0719753be3 Set PARALLEL_UPDATES and update quality_scale for Miele integration (#144770)
Set PARALLEL_UPDATES and update quality_scale
2025-05-12 23:53:54 +02:00
Franck Nijhof ba3181d4e7 Update pipdeptree to 2.26.1 (#144775) 2025-05-12 23:52:27 +02:00
Guido Schmitz e58750555e Rework platform setup tests for devolo Home Network (#143114)
* Rework platform setup tests for devolo Home Network

* Fix sensor test

* Remove unload
2025-05-12 23:21:14 +02:00
Guido Schmitz 026687299d Assert resulting data in devolo Home Network test_form_reauth (#144760) 2025-05-12 21:28:40 +02:00
Martin Hjelmare 3eed552c56 Repair Z-Wave unknown controller (#144738)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-05-12 21:18:55 +02:00
J. Diego Rodríguez Royo 15a4514c7d Add MAC connection through DHCP discovery to Home Connect devices (#144611)
* Add MAC connection through DHCP discovery to Home Connect devices

* Update snapshots
2025-05-12 21:11:12 +02:00
Joakim Sørensen b5445c0061 Allow subscription_expired repair issue in cloud (#144316)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-12 19:48:20 +02:00
Øyvind Matheson Wergeland 1d0584a90d Bump gcal-sync to 7.0.1 (#144718)
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-05-12 19:45:34 +02:00
Matthias Alphart 158b795c70 Update xknx to 3.8.0 (#144753) 2025-05-12 19:45:02 +02:00
Paulus Schoutsen 4994229215 Track if TTS entity supports streaming input (#144697)
* Track if entity supports streaming

* Make class method
2025-05-12 13:44:39 -04:00
Andre Lengwenus c022c32d2f Simplify unique config_entry check for LCN (#135756)
* Simplify check for unique config_entry

* Fix tests

* Fix reconfigure flow

* Add check for unchanging IP/port combination

* Remove explicit check for unchanged IP/port combination
2025-05-12 19:44:24 +02:00
Joost Lekkerkerker d2ef3ca100 Fill in Plaato URL via placeholders (#144754) 2025-05-12 19:37:45 +02:00
Manu 00faadcfea Improve config flow description in ntfy integration (#144581) 2025-05-12 19:36:53 +02:00
Norbert Rittel a6ff52b300 Fix outdated help center URL in plaato (#144748)
* Fix outdated help center URL in `plaato`

* Remove excessive space character
2025-05-12 19:12:49 +02:00
Joakim Sørensen da0d65ca5b Log instead of ValueError for missing cloud translation key (#144732)
* Log instead of ValueError for missing translation key

* Update homeassistant/components/cloud/client.py
2025-05-12 18:59:38 +02:00
Paulus Schoutsen 2266e97417 Add a test for Assist Pipeline streaming deltas to TTS (#144711)
* Add a test for Assist Pipeline streaming deltas to TTS

* Adjust tests to new TTS engine
2025-05-12 12:15:05 -04:00
Norbert Rittel d471de5645 Spelling fixes in user-facing strings of fronius (#144744) 2025-05-12 16:54:22 +02:00
Norbert Rittel 38674f0dc2 Add missing hyphen to "password-protected" in Shelly (#144746) 2025-05-12 17:47:14 +03:00
Erik Montnemery b192ca4bad Make it possible to subscribe to frontend user store (#144724) 2025-05-12 16:01:42 +02:00
epenet 73a59523f5 Merge websocket test constants in samsungtv tests (#144741) 2025-05-12 15:51:21 +02:00
Erik Montnemery 05324dedd0 Deduplicate condition schemas (#144739) 2025-05-12 15:38:31 +02:00
Norbert Rittel f1e5f73d7e Improve user-facing strings of velbus (#144716)
- add the missing hyphen to "password-protected"
- resolve missing genitive in `sync_clock` action description
- resolve singular/plural mismatch in `set_memo_text` action description
2025-05-12 16:35:06 +03:00
Robert Resch 7b23f21712 Remove deprecated camera async_handle_web_rtc_offer function (#144561) 2025-05-12 14:47:49 +02:00
epenet 4dde314338 Remove obsolete tests in SamsungTV (#144735) 2025-05-12 13:45:20 +02:00
Erik Montnemery cba12fb598 Refactor frontend user store (#144723)
* Refactor frontend user store

* Address review comments
2025-05-12 12:00:32 +02:00
epenet 63e38b4d8d Rename samsung encrypted websocket test fixtures and constants (#144726)
* Rename samsung encrypted websocket test fixtures and constants

* More

* More
2025-05-12 11:36:22 +02:00
Simone Chemelli 7eded95315 Bump aiocomelit to 0.12.1 (#144720) 2025-05-12 11:23:44 +02:00
epenet e493fe1105 Rename samsung websocket test fixtures and constants (#144719) 2025-05-12 10:27:29 +02:00
Åke Strandberg 646c230940 Add target temp sensor to Miele washing machines (#144507) 2025-05-12 09:42:27 +02:00
Thomas55555 5276a3688e Fix wrong state in Husqvarna Automower (#144684) 2025-05-12 09:39:30 +02:00
Allen Porter 0616bf16f4 Bump ical to 9.2.2 (#144713) 2025-05-12 09:37:57 +02:00
epenet fbe1811e2b Improve SamsungTV test coverage (#144717) 2025-05-12 09:23:55 +02:00
epenet 2333c10915 Rename samsung legacy test fixtures and constants (#144715)
* Rename samsung legacy test fixtures and constants

* More
2025-05-12 09:13:23 +02:00
93 changed files with 3249 additions and 2638 deletions
+10 -66
View File
@@ -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
+7 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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",
+95 -50
View File
@@ -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
+1 -1
View File
@@ -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"
],
+18 -27
View File
@@ -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(
+3 -3
View File
@@ -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__)
+2
View File
@@ -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__)
+2
View File
@@ -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"
},
+2
View File
@@ -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
+26
View File
@@ -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"
}
+2
View File
@@ -28,6 +28,8 @@ from .const import (
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
+2
View File
@@ -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.
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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"]
}
+19 -2
View File
@@ -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:
+1 -1
View File
@@ -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.",
+136
View File
@@ -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
+14 -2
View File
@@ -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
+7
View File
@@ -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."""
+3 -3
View File
@@ -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": {
+4 -4
View File
@@ -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:
+4 -105
View File
@@ -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],
+40 -30
View File
@@ -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 -1
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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"
+21 -2
View File
@@ -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',
+162 -11
View File
@@ -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,
+11 -6
View File
@@ -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())
+5 -12
View File
@@ -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 -256
View File
@@ -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"},
}
+11 -10
View File
@@ -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)
+119 -2
View File
@@ -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(
+2 -2
View File
@@ -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(
+48 -41
View File
@@ -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)
+14 -23
View File
@@ -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({
+160 -111
View File
@@ -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
+32 -70
View File
@@ -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
+27 -23
View File
@@ -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 == {
+9 -9
View File
@@ -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
+1
View File
@@ -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],
+33
View File
@@ -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 -1
View File
@@ -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
+5 -1
View File
@@ -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)
+116
View File
@@ -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
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -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)