mirror of
https://github.com/home-assistant/core.git
synced 2026-08-30 10:13:25 -05:00
Fixed device key retrieval issue for Midea (#177766)
This commit is contained in:
committed by
Bram Kragten
parent
4b35e289a8
commit
bbff34fbdd
@@ -1,5 +1,6 @@
|
||||
"""Config flow for Midea."""
|
||||
|
||||
from functools import partial
|
||||
from operator import itemgetter
|
||||
from typing import Any, override
|
||||
|
||||
@@ -11,6 +12,7 @@ from midealocal.cloud import (
|
||||
)
|
||||
from midealocal.const import DeviceType, ProtocolVersion
|
||||
from midealocal.device import MideaDevice
|
||||
from midealocal.devices import device_selector
|
||||
from midealocal.discover import discover
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -47,6 +49,40 @@ def _connect_and_close(dm: MideaDevice) -> bool:
|
||||
dm.close_socket()
|
||||
|
||||
|
||||
def _select_and_connect(
|
||||
*,
|
||||
device_id: int,
|
||||
device_type: int,
|
||||
ip_address: str,
|
||||
port: int,
|
||||
token: str,
|
||||
key: str,
|
||||
device_protocol: ProtocolVersion,
|
||||
model: str,
|
||||
subtype: int,
|
||||
) -> bool | None:
|
||||
"""Select the device implementation and connect to it in a single executor job.
|
||||
|
||||
Returns None if there is no device implementation for device_type.
|
||||
"""
|
||||
dm = device_selector(
|
||||
"",
|
||||
device_id,
|
||||
device_type,
|
||||
ip_address,
|
||||
port,
|
||||
token,
|
||||
key,
|
||||
device_protocol,
|
||||
model,
|
||||
subtype,
|
||||
"",
|
||||
)
|
||||
if dm is None:
|
||||
return None
|
||||
return _connect_and_close(dm)
|
||||
|
||||
|
||||
class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Define current integration setup steps.
|
||||
|
||||
@@ -366,22 +402,30 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
keys = await self.cloud.get_cloud_keys(appliance_id)
|
||||
if default_key:
|
||||
keys = {**keys, **(await MideaCloud.get_default_keys())}
|
||||
error = "connect_error"
|
||||
# use token/key to connect device and confirm token result
|
||||
for k, value in keys.items():
|
||||
dm = MideaDevice(
|
||||
name="",
|
||||
device_id=appliance_id,
|
||||
device_type=device.get(CONF_TYPE),
|
||||
ip_address=device.get(CONF_IP_ADDRESS),
|
||||
port=device.get(CONF_PORT),
|
||||
token=value["token"],
|
||||
key=value["key"],
|
||||
device_protocol=ProtocolVersion.V3,
|
||||
model=device.get(CONF_MODEL),
|
||||
subtype=device.get(CONF_SUBTYPE, 0),
|
||||
attributes={},
|
||||
connected = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
_select_and_connect,
|
||||
device_id=appliance_id,
|
||||
device_type=device.get(CONF_TYPE),
|
||||
ip_address=device.get(CONF_IP_ADDRESS),
|
||||
port=device.get(CONF_PORT),
|
||||
token=value["token"],
|
||||
key=value["key"],
|
||||
device_protocol=ProtocolVersion.V3,
|
||||
model=device.get(CONF_MODEL),
|
||||
subtype=device.get(CONF_SUBTYPE, 0),
|
||||
),
|
||||
)
|
||||
connected = await self.hass.async_add_executor_job(_connect_and_close, dm)
|
||||
if connected is None:
|
||||
LOGGER.debug(
|
||||
"No device implementation for device_type %s",
|
||||
device.get(CONF_TYPE),
|
||||
)
|
||||
error = "no_device_implementation"
|
||||
break
|
||||
if connected:
|
||||
return value
|
||||
# return debug log with failed key
|
||||
@@ -392,7 +436,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.debug(
|
||||
"Unable to connect device with all the token/key",
|
||||
)
|
||||
return {"error": "connect_error"}
|
||||
return {"error": error}
|
||||
|
||||
async def async_step_auto(
|
||||
self,
|
||||
@@ -512,20 +556,20 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(str(device_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
dm = MideaDevice(
|
||||
name="",
|
||||
device_id=device_id,
|
||||
device_type=user_input[CONF_TYPE],
|
||||
ip_address=user_input[CONF_IP_ADDRESS],
|
||||
port=user_input[CONF_PORT],
|
||||
token=user_input[CONF_TOKEN],
|
||||
key=user_input[CONF_KEY],
|
||||
device_protocol=user_input[CONF_PROTOCOL],
|
||||
model=user_input[CONF_MODEL],
|
||||
subtype=user_input[CONF_SUBTYPE],
|
||||
attributes={},
|
||||
connected = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
_select_and_connect,
|
||||
device_id=device_id,
|
||||
device_type=user_input[CONF_TYPE],
|
||||
ip_address=user_input[CONF_IP_ADDRESS],
|
||||
port=user_input[CONF_PORT],
|
||||
token=user_input[CONF_TOKEN],
|
||||
key=user_input[CONF_KEY],
|
||||
device_protocol=user_input[CONF_PROTOCOL],
|
||||
model=user_input[CONF_MODEL],
|
||||
subtype=user_input[CONF_SUBTYPE],
|
||||
),
|
||||
)
|
||||
connected = await self.hass.async_add_executor_job(_connect_and_close, dm)
|
||||
if connected:
|
||||
device_type = user_input[CONF_TYPE]
|
||||
found_name = self.found_device.get(CONF_NAME)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"""Tests for the Midea config flow."""
|
||||
|
||||
from functools import partial
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from midealocal.const import DeviceType, ProtocolVersion
|
||||
from midealocal.device import MideaDevice
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.midea.config_flow import (
|
||||
DEFAULT_CLOUD,
|
||||
LOGIN_MODE_ACCOUNT,
|
||||
LOGIN_MODE_PRESET,
|
||||
_select_and_connect,
|
||||
)
|
||||
from homeassistant.components.midea.const import (
|
||||
CONF_ACCOUNT,
|
||||
@@ -69,12 +72,12 @@ async def test_manual_flow_success(hass: HomeAssistant) -> None:
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
) as mock_midea_device,
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
) as mock_device_selector,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.connect.return_value = True
|
||||
mock_midea_device.return_value = mock_device
|
||||
mock_device_selector.return_value = mock_device
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
@@ -128,12 +131,12 @@ async def test_manual_flow_duplicate_unique_id(hass: HomeAssistant) -> None:
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
) as mock_midea_device,
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
) as mock_device_selector,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.connect.return_value = True
|
||||
mock_midea_device.return_value = mock_device
|
||||
mock_device_selector.return_value = mock_device
|
||||
|
||||
await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
@@ -317,7 +320,7 @@ async def test_manual_step_errors(
|
||||
return_value=discover_result,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
patch(
|
||||
@@ -416,7 +419,7 @@ async def test_manual_step_retries_discovery_after_mismatch(
|
||||
],
|
||||
) as mock_discover,
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -532,7 +535,7 @@ async def test_auto_flow_cloud_device_info_overrides_name_and_subtype(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -604,7 +607,7 @@ async def test_auto_flow_v3_preset_phase1_cloud_keys_success(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -673,7 +676,7 @@ async def test_auto_flow_v3_preset_phase1_default_key_success(
|
||||
AsyncMock(return_value={"builtin": {"token": TEST_TOKEN, "key": TEST_KEY}}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -748,7 +751,7 @@ async def test_auto_flow_v3_token_retrieval_exhausted(hass: HomeAssistant) -> No
|
||||
AsyncMock(return_value={"builtin": {"token": TEST_TOKEN, "key": TEST_KEY}}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -949,7 +952,7 @@ async def test_auto_flow_v3_phase2_success_after_phase1_failure(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -1079,7 +1082,7 @@ async def test_auto_flow_v1_v2_success_when_cloud_down(
|
||||
return_value=mock_devices,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
patch(
|
||||
@@ -1307,7 +1310,7 @@ async def test_login_credentials_step_recovers_after_failed_login(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
@@ -1474,10 +1477,10 @@ async def test_manual_step_v3_missing_token_key_sets_retrieved_values(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
) as mock_midea_device,
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
) as mock_device_selector,
|
||||
):
|
||||
mock_midea_device.return_value = dm
|
||||
mock_device_selector.return_value = dm
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input=user_input,
|
||||
@@ -1487,8 +1490,88 @@ async def test_manual_step_v3_missing_token_key_sets_retrieved_values(
|
||||
assert result["step_id"] == "manually"
|
||||
assert result["errors"] == {"base": "device_auth_failed"}
|
||||
|
||||
assert mock_midea_device.call_args.kwargs["token"] == TEST_TOKEN
|
||||
assert mock_midea_device.call_args.kwargs["key"] == TEST_KEY
|
||||
# _select_and_connect() calls device_selector() positionally (name,
|
||||
# device_id, device_type, ip_address, port, token, key, ...); it is itself
|
||||
# submitted to hass.async_add_executor_job via functools.partial so device
|
||||
# selection and the connection attempt share a single executor job.
|
||||
assert mock_device_selector.call_args.args[5] == TEST_TOKEN
|
||||
assert mock_device_selector.call_args.args[6] == TEST_KEY
|
||||
|
||||
|
||||
async def test_manual_step_v3_missing_token_key_unsupported_device_type(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test cloud key retrieval surfaces token_unavailable when device_selector() finds no module.
|
||||
|
||||
device_selector() returns None when there is no device implementation
|
||||
module for the given device_type. _check_key_from_cloud() must stop
|
||||
trying candidate keys and report no usable token, rather than crashing
|
||||
on a None device.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
flow_id = result["flow_id"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"next_step_id": "manually"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manually"
|
||||
|
||||
device = {
|
||||
**BASE_DATA,
|
||||
CONF_TYPE: TEST_TYPE,
|
||||
CONF_PROTOCOL: ProtocolVersion.V3,
|
||||
CONF_IP_ADDRESS: TEST_IP_ADDRESS,
|
||||
CONF_SUBTYPE: TEST_SUBTYPE,
|
||||
}
|
||||
user_input: dict[str, object] = {
|
||||
**EXTENDED_DATA,
|
||||
CONF_PROTOCOL: ProtocolVersion.V3,
|
||||
CONF_TOKEN: "",
|
||||
CONF_KEY: "",
|
||||
}
|
||||
|
||||
cloud = MagicMock()
|
||||
cloud.login = AsyncMock(return_value=True)
|
||||
cloud.get_cloud_keys = AsyncMock(
|
||||
return_value={"method": {"token": TEST_TOKEN, "key": TEST_KEY}}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value={TEST_DEVICE_ID: device},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.async_get_clientsession",
|
||||
return_value=object(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.get_midea_cloud",
|
||||
return_value=cloud,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaCloud.get_default_keys",
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manually"
|
||||
assert result["errors"] == {"base": "token_unavailable"}
|
||||
|
||||
|
||||
async def test_manually_flow_success(hass: HomeAssistant) -> None:
|
||||
@@ -1514,12 +1597,12 @@ async def test_manually_flow_success(hass: HomeAssistant) -> None:
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
) as mock_midea_device,
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
) as mock_device_selector,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.connect.return_value = True
|
||||
mock_midea_device.return_value = mock_device
|
||||
mock_device_selector.return_value = mock_device
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
@@ -1544,6 +1627,157 @@ async def test_manually_flow_success(hass: HomeAssistant) -> None:
|
||||
assert result["data"][CONF_KEY] == TEST_KEY
|
||||
|
||||
|
||||
async def test_manually_flow_unsupported_device_type(hass: HomeAssistant) -> None:
|
||||
"""Test entry creation surfaces device_auth_failed when device_selector() finds no module.
|
||||
|
||||
device_selector() returns None when there is no device implementation
|
||||
module for the given device_type, which must be treated the same as a
|
||||
failed connection rather than crashing on a None device.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
flow_id = result["flow_id"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"next_step_id": "manually"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manually"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={**EXTENDED_DATA},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manually"
|
||||
assert result["errors"] == {"base": "device_auth_failed"}
|
||||
|
||||
|
||||
async def test_manually_flow_builds_concrete_device_subclass(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test entry creation builds a concrete device subclass, not the base class.
|
||||
|
||||
Regression test: _async_create_midea_entry previously instantiated the
|
||||
abstract midealocal.device.MideaDevice directly instead of going through
|
||||
device_selector(). Its build_query() unconditionally raises
|
||||
NotImplementedError, so refresh_status() would fail immediately after a
|
||||
successful authentication for every real device.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
flow_id = result["flow_id"]
|
||||
|
||||
await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"next_step_id": "manually"},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch.object(MideaDevice, "connect", autospec=True) as mock_connect,
|
||||
patch.object(MideaDevice, "close_socket", autospec=True),
|
||||
):
|
||||
mock_connect.return_value = True
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={
|
||||
CONF_DEVICE_ID: TEST_DEVICE_ID,
|
||||
CONF_TYPE: TEST_TYPE,
|
||||
CONF_IP_ADDRESS: TEST_IP_ADDRESS,
|
||||
CONF_PORT: TEST_PORT,
|
||||
CONF_PROTOCOL: TEST_PROTOCOL,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_SUBTYPE: TEST_SUBTYPE,
|
||||
CONF_TOKEN: TEST_TOKEN,
|
||||
CONF_KEY: TEST_KEY,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
dm = mock_connect.call_args.args[0]
|
||||
assert type(dm) is not MideaDevice
|
||||
assert isinstance(dm.build_query(), list)
|
||||
|
||||
|
||||
async def test_manually_flow_runs_device_selector_in_executor(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test device_selector() is dispatched via the executor, not the event loop.
|
||||
|
||||
Regression test: device_selector() calls importlib.import_module() to
|
||||
dynamically load the concrete device subclass. That is a blocking call,
|
||||
so it must never run directly on the event loop - Home Assistant's
|
||||
blocking-call detector flags exactly that. device_selector() is invoked
|
||||
from within _select_and_connect(), which is what actually gets dispatched
|
||||
to the executor (wrapped in a functools.partial).
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
flow_id = result["flow_id"]
|
||||
|
||||
await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={"next_step_id": "manually"},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.discover",
|
||||
return_value=DISCOVERY_RESULT,
|
||||
),
|
||||
patch.object(MideaDevice, "connect", autospec=True, return_value=True),
|
||||
patch.object(MideaDevice, "close_socket", autospec=True),
|
||||
patch.object(
|
||||
hass,
|
||||
"async_add_executor_job",
|
||||
wraps=hass.async_add_executor_job,
|
||||
) as mock_executor_job,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id,
|
||||
user_input={
|
||||
CONF_DEVICE_ID: TEST_DEVICE_ID,
|
||||
CONF_TYPE: TEST_TYPE,
|
||||
CONF_IP_ADDRESS: TEST_IP_ADDRESS,
|
||||
CONF_PORT: TEST_PORT,
|
||||
CONF_PROTOCOL: TEST_PROTOCOL,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_SUBTYPE: TEST_SUBTYPE,
|
||||
CONF_TOKEN: TEST_TOKEN,
|
||||
CONF_KEY: TEST_KEY,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
dispatched_funcs = [call.args[0] for call in mock_executor_job.call_args_list]
|
||||
assert any(
|
||||
isinstance(func, partial) and func.func is _select_and_connect
|
||||
for func in dispatched_funcs
|
||||
)
|
||||
|
||||
|
||||
async def test_login_credentials_step_falls_back_to_default_cloud(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -1663,7 +1897,7 @@ async def test_login_credentials_step_success_resumes_auto_flow(
|
||||
AsyncMock(return_value={}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.midea.config_flow.MideaDevice",
|
||||
"homeassistant.components.midea.config_flow.device_selector",
|
||||
return_value=dm,
|
||||
),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user